Add buefy support for quick-printing product labels; also speed bump

This commit is contained in:
Lance Edgar 2022-01-09 15:20:35 -06:00
parent 94fc5c1859
commit 0545099a2b
7 changed files with 167 additions and 36 deletions

View file

@ -2,7 +2,7 @@
################################################################################ ################################################################################
# #
# Rattail -- Retail Software Framework # Rattail -- Retail Software Framework
# Copyright © 2010-2021 Lance Edgar # Copyright © 2010-2022 Lance Edgar
# #
# This file is part of Rattail. # This file is part of Rattail.
# #
@ -197,7 +197,7 @@ class Grid(object):
""" """
Mark the given column as "invisible" (but do not remove it). Mark the given column as "invisible" (but do not remove it).
Use :meth:`hide_column()` if you actually want to remove it. Use :meth:`remove()` if you actually want to remove it.
""" """
if invisible: if invisible:
if key not in self.invisible: if key not in self.invisible:
@ -217,7 +217,7 @@ class Grid(object):
def replace(self, oldfield, newfield): def replace(self, oldfield, newfield):
self.insert_after(oldfield, newfield) self.insert_after(oldfield, newfield)
self.hide_column(oldfield) self.remove(oldfield)
def set_joiner(self, key, joiner): def set_joiner(self, key, joiner):
if joiner is None: if joiner is None:

View file

@ -21,7 +21,8 @@
} else { } else {
this.$buefy.toast.open({ this.$buefy.toast.open({
message: "Submit failed: " + response.data.error, message: "Submit failed: " + (response.data.error ||
"(unknown error)"),
type: 'is-danger', type: 'is-danger',
duration: 4000, // 4 seconds duration: 4000, // 4 seconds
}) })

View file

@ -162,9 +162,6 @@
<li>${h.link_to("Create a new {}".format(model_title), url('{}.create'.format(route_prefix)))}</li> <li>${h.link_to("Create a new {}".format(model_title), url('{}.create'.format(route_prefix)))}</li>
% endif % endif
% endif % endif
% if not use_buefy and master.configurable and master.has_perm('configure'):
<li>${h.link_to("Configure {}".format(config_title), url('{}.configure'.format(route_prefix)))}</li>
% endif
% if master.has_input_file_templates and master.has_perm('create'): % if master.has_input_file_templates and master.has_perm('create'):
% for template in six.itervalues(input_file_templates): % for template in six.itervalues(input_file_templates):
<li>${h.link_to("Download {} Template".format(template['label']), template['effective_url'])}</li> <li>${h.link_to("Download {} Template".format(template['label']), template['effective_url'])}</li>

View file

@ -3,7 +3,7 @@
<%def name="form_content()"> <%def name="form_content()">
<h3 class="block is-size-3">Key Field</h3> <h3 class="block is-size-3">Display</h3>
<div class="block" style="padding-left: 2rem;"> <div class="block" style="padding-left: 2rem;">
<b-field grouped> <b-field grouped>
@ -27,6 +27,15 @@
</b-field> </b-field>
<b-field message="If a product has an image in the DB, that will still be preferred.">
<b-checkbox name="tailbone.products.show_pod_image"
v-model="simpleSettings['tailbone.products.show_pod_image']"
native-value="true"
@input="settingsNeedSaved = true">
Show "POD" Images as fallback
</b-checkbox>
</b-field>
</div> </div>
<h3 class="block is-size-3">Handling</h3> <h3 class="block is-size-3">Handling</h3>
@ -43,18 +52,28 @@
</div> </div>
<h3 class="block is-size-3">Display</h3> <h3 class="block is-size-3">Labels</h3>
<div class="block" style="padding-left: 2rem;"> <div class="block" style="padding-left: 2rem;">
<b-field message="If a product has an image in the DB, that will still be preferred."> <b-field message="User must also have permission to use this feature.">
<b-checkbox name="tailbone.products.show_pod_image" <b-checkbox name="tailbone.products.print_labels"
v-model="simpleSettings['tailbone.products.show_pod_image']" v-model="simpleSettings['tailbone.products.print_labels']"
native-value="true" native-value="true"
@input="settingsNeedSaved = true"> @input="settingsNeedSaved = true">
Show "POD" Images as fallback Allow quick/direct label printing from Products page
</b-checkbox> </b-checkbox>
</b-field> </b-field>
<b-field label="Speed Bump Threshold"
message="Show speed bump when at least this many labels are quick-printed at once. Empty means never show speed bump.">
<b-input name="tailbone.products.quick_labels.speedbump_threshold"
v-model="simpleSettings['tailbone.products.quick_labels.speedbump_threshold']"
type="number"
@input="settingsNeedSaved = true"
style="width: 10rem;">
</b-input>
</b-field>
</div> </div>
</%def> </%def>

View file

@ -1,8 +1,9 @@
## -*- coding: utf-8 -*- ## -*- coding: utf-8; -*-
<%inherit file="/master/index.mako" /> <%inherit file="/master/index.mako" />
<%def name="extra_styles()"> <%def name="extra_styles()">
${parent.extra_styles()} ${parent.extra_styles()}
% if not use_buefy:
<style type="text/css"> <style type="text/css">
table.label-printing th { table.label-printing th {
@ -32,11 +33,12 @@
} }
</style> </style>
% endif
</%def> </%def>
<%def name="extra_javascript()"> <%def name="extra_javascript()">
${parent.extra_javascript()} ${parent.extra_javascript()}
% if label_profiles and request.has_perm('products.print_labels'): % if not use_buefy and label_profiles and master.has_perm('print_labels'):
<script type="text/javascript"> <script type="text/javascript">
$(function() { $(function() {
@ -52,6 +54,14 @@
quantity.focus(); quantity.focus();
} else { } else {
quantity = quantity.val(); quantity = quantity.val();
var threshold = ${json.dumps(quick_label_speedbump_threshold)|n};
if (threshold && parseInt(quantity) >= threshold) {
if (!confirm("Are you sure you want to print " + quantity + " labels?")) {
return false;
}
}
var data = { var data = {
product: tr.data('uuid'), product: tr.data('uuid'),
profile: $('#label-profile').val(), profile: $('#label-profile').val(),
@ -77,7 +87,26 @@
<%def name="grid_tools()"> <%def name="grid_tools()">
${parent.grid_tools()} ${parent.grid_tools()}
% if label_profiles and request.has_perm('products.print_labels'): % if label_profiles and master.has_perm('print_labels'):
% if use_buefy:
<b-field grouped>
<b-field label="Label">
<b-select v-model="quickLabelProfile">
% for profile in label_profiles:
<option value="${profile.uuid}">
${profile.description}
</option>
% endfor
</b-select>
</b-field>
<b-field label="Qty.">
<b-input v-model="quickLabelQuantity"
ref="quickLabelQuantityInput"
style="width: 4rem;">
</b-input>
</b-field>
</b-field>
% else:
<table class="label-printing"> <table class="label-printing">
<thead> <thead>
<tr> <tr>
@ -98,7 +127,69 @@
</td> </td>
</tbody> </tbody>
</table> </table>
% endif
% endif % endif
</%def> </%def>
<%def name="render_grid_component()">
<${grid.component} :csrftoken="csrftoken"
% if master.deletable and master.has_perm('delete') and master.delete_confirm == 'simple':
@deleteActionClicked="deleteObject"
% endif
% if label_profiles and master.has_perm('print_labels'):
@quick-label-print="quickLabelPrint"
% endif
>
</${grid.component}>
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
% if label_profiles and master.has_perm('print_labels'):
<script type="text/javascript">
${grid.component_studly}Data.quickLabelProfile = ${json.dumps(label_profiles[0].uuid)|n}
${grid.component_studly}Data.quickLabelQuantity = 1
${grid.component_studly}Data.quickLabelSpeedbumpThreshold = ${json.dumps(quick_label_speedbump_threshold)|n}
${grid.component_studly}.methods.quickLabelPrint = function(row) {
let quantity = parseInt(this.quickLabelQuantity)
if (isNaN(quantity)) {
alert("You must provide a valid label quantity.")
this.$refs.quickLabelQuantityInput.focus()
return
}
if (this.quickLabelSpeedbumpThreshold && quantity >= this.quickLabelSpeedbumpThreshold) {
if (!confirm("Are you sure you want to print " + quantity + " labels?")) {
return
}
}
this.$emit('quick-label-print', row.uuid, this.quickLabelProfile, quantity)
}
ThisPage.methods.quickLabelPrint = function(product, profile, quantity) {
let url = '${url('products.print_labels')}'
let data = new FormData()
data.append('product', product)
data.append('profile', profile)
data.append('quantity', quantity)
this.submitForm(url, data, response => {
if (quantity == 1) {
alert("1 label has been printed.")
} else {
alert(quantity.toString() + " labels have been printed.")
}
})
}
</script>
% endif
</%def>
${parent.body()} ${parent.body()}

View file

@ -4368,6 +4368,8 @@ class MasterView(View):
if simple.get('type') is bool: if simple.get('type') is bool:
value = six.text_type(bool(value)).lower() value = six.text_type(bool(value)).lower()
elif simple.get('type') is int:
value = six.text_type(int(value or '0'))
else: else:
value = six.text_type(value) value = six.text_type(value)

View file

@ -181,7 +181,9 @@ class ProductView(MasterView):
self.print_labels = request.rattail_config.getbool('tailbone', 'products.print_labels', default=False) self.print_labels = request.rattail_config.getbool('tailbone', 'products.print_labels', default=False)
app = self.get_rattail_app() app = self.get_rattail_app()
self.handler = app.get_products_handler() self.product_handler = app.get_products_handler()
# TODO: deprecate / remove this
self.handler = self.product_handler
def query(self, session): def query(self, session):
user = self.request.user user = self.request.user
@ -358,8 +360,13 @@ class ProductView(MasterView):
g.set_sort_defaults('upc') g.set_sort_defaults('upc')
if self.print_labels and self.request.has_perm('products.print_labels'): if self.print_labels and self.has_perm('print_labels'):
g.more_actions.append(grids.GridAction('print_label', icon='print')) if use_buefy:
g.more_actions.append(self.make_action(
'print_label', icon='print', url='#',
click_handler='quickLabelPrint(props.row)'))
else:
g.more_actions.append(grids.GridAction('print_label', icon='print'))
g.set_type('upc', 'gpc') g.set_type('upc', 'gpc')
@ -522,7 +529,7 @@ class ProductView(MasterView):
if not product.not_for_sale: if not product.not_for_sale:
price = product[field] price = product[field]
if price: if price:
return self.handler.render_price(price) return self.product_handler.render_price(price)
def render_current_price_for_grid(self, product, field): def render_current_price_for_grid(self, product, field):
text = self.render_price(product, field) or "" text = self.render_price(product, field) or ""
@ -651,13 +658,20 @@ class ProductView(MasterView):
return pretty_quantity(inventory.on_order) return pretty_quantity(inventory.on_order)
def template_kwargs_index(self, **kwargs): def template_kwargs_index(self, **kwargs):
if self.print_labels: kwargs = super(ProductView, self).template_kwargs_index(**kwargs)
kwargs['label_profiles'] = Session.query(model.LabelProfile)\ model = self.model
.filter(model.LabelProfile.visible == True)\
.order_by(model.LabelProfile.ordinal)\
.all()
return kwargs
if self.print_labels:
kwargs['label_profiles'] = self.Session.query(model.LabelProfile)\
.filter(model.LabelProfile.visible == True)\
.order_by(model.LabelProfile.ordinal)\
.all()
kwargs['quick_label_speedbump_threshold'] = self.rattail_config.getint(
'tailbone', 'products.quick_labels.speedbump_threshold')
return kwargs
def grid_extra_class(self, product, i): def grid_extra_class(self, product, i):
classes = [] classes = []
@ -794,10 +808,10 @@ class ProductView(MasterView):
def get_instance(self): def get_instance(self):
key = self.request.matchdict['uuid'] key = self.request.matchdict['uuid']
product = Session.query(model.Product).get(key) product = self.Session.query(model.Product).get(key)
if product: if product:
return product return product
price = Session.query(model.ProductPrice).get(key) price = self.Session.query(model.ProductPrice).get(key)
if price: if price:
return price.product return price.product
raise httpexceptions.HTTPNotFound() raise httpexceptions.HTTPNotFound()
@ -1151,7 +1165,7 @@ class ProductView(MasterView):
product = kwargs['instance'] product = kwargs['instance']
use_buefy = self.get_use_buefy() use_buefy = self.get_use_buefy()
kwargs['image_url'] = self.handler.get_image_url(product) kwargs['image_url'] = self.product_handler.get_image_url(product)
kwargs['product_key_field'] = self.rattail_config.product_key() kwargs['product_key_field'] = self.rattail_config.product_key()
# add price history, if user has access # add price history, if user has access
@ -1701,11 +1715,11 @@ class ProductView(MasterView):
upc = self.request.GET.get('upc', '').strip() upc = self.request.GET.get('upc', '').strip()
upc = re.sub(r'\D', '', upc) upc = re.sub(r'\D', '', upc)
if upc: if upc:
product = api.get_product_by_upc(Session(), upc) product = api.get_product_by_upc(self.Session(), upc)
if not product: if not product:
# Try again, assuming caller did not include check digit. # Try again, assuming caller did not include check digit.
upc = GPC(upc, calc_check_digit='upc') upc = GPC(upc, calc_check_digit='upc')
product = api.get_product_by_upc(Session(), upc) product = api.get_product_by_upc(self.Session(), upc)
if product and (not product.deleted or self.request.has_perm('products.view_deleted')): if product and (not product.deleted or self.request.has_perm('products.view_deleted')):
data = { data = {
'uuid': product.uuid, 'uuid': product.uuid,
@ -1716,7 +1730,7 @@ class ProductView(MasterView):
} }
uuid = self.request.GET.get('with_vendor_cost') uuid = self.request.GET.get('with_vendor_cost')
if uuid: if uuid:
vendor = Session.query(model.Vendor).get(uuid) vendor = self.Session.query(model.Vendor).get(uuid)
if not vendor: if not vendor:
return {'error': "Vendor not found"} return {'error': "Vendor not found"}
cost = product.cost_for_vendor(vendor) cost = product.cost_for_vendor(vendor)
@ -1912,21 +1926,28 @@ class ProductView(MasterView):
def configure_get_simple_settings(self): def configure_get_simple_settings(self):
return [ return [
# key field # display
{'section': 'rattail', {'section': 'rattail',
'option': 'product.key'}, 'option': 'product.key'},
{'section': 'rattail', {'section': 'rattail',
'option': 'product.key_title'}, 'option': 'product.key_title'},
{'section': 'tailbone',
'option': 'products.show_pod_image',
'type': bool},
# handling # handling
{'section': 'rattail', {'section': 'rattail',
'option': 'products.convert_type2_for_gpc_lookup', 'option': 'products.convert_type2_for_gpc_lookup',
'type': bool}, 'type': bool},
# display # labels
{'section': 'tailbone', {'section': 'tailbone',
'option': 'products.show_pod_image', 'option': 'products.print_labels',
'type': bool}, 'type': bool},
{'section': 'tailbone',
'option': 'products.quick_labels.speedbump_threshold',
'type': int},
] ]
@classmethod @classmethod
@ -2254,7 +2275,7 @@ def print_labels(request):
except Exception as error: except Exception as error:
log.warning("error occurred while printing labels", exc_info=True) log.warning("error occurred while printing labels", exc_info=True)
return {'error': six.text_type(error)} return {'error': six.text_type(error)}
return {} return {'ok': True}
def includeme(config): def includeme(config):