Add buefy support for quick-printing product labels; also speed bump
This commit is contained in:
parent
94fc5c1859
commit
0545099a2b
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
@ -99,6 +128,68 @@
|
||||||
</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()}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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,7 +360,12 @@ 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'):
|
||||||
|
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.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):
|
||||||
|
kwargs = super(ProductView, self).template_kwargs_index(**kwargs)
|
||||||
|
model = self.model
|
||||||
|
|
||||||
if self.print_labels:
|
if self.print_labels:
|
||||||
kwargs['label_profiles'] = Session.query(model.LabelProfile)\
|
|
||||||
|
kwargs['label_profiles'] = self.Session.query(model.LabelProfile)\
|
||||||
.filter(model.LabelProfile.visible == True)\
|
.filter(model.LabelProfile.visible == True)\
|
||||||
.order_by(model.LabelProfile.ordinal)\
|
.order_by(model.LabelProfile.ordinal)\
|
||||||
.all()
|
.all()
|
||||||
return kwargs
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
|
Loading…
Reference in a new issue