Refactor mobile receiving to use "quick row" feature
plus some other random things thrown in there, for good measure..
This commit is contained in:
parent
3cc8adba86
commit
a34a42d2b2
|
@ -113,7 +113,6 @@ $(document).on('click', '#datasync-restart', function() {
|
|||
// handle global keypress on product batch "row" page, for sake of scanner wedge
|
||||
var product_batch_routes = [
|
||||
'mobile.batch.inventory.view',
|
||||
'mobile.receiving.view',
|
||||
];
|
||||
$(document).on('keypress', function(event) {
|
||||
var current_route = $('.ui-page-active [role="main"]').data('route');
|
||||
|
@ -146,10 +145,12 @@ $(document).on('keypress', function(event) {
|
|||
});
|
||||
|
||||
|
||||
// handle ENTER press for quick_row forms
|
||||
// handle various keypress events for quick row forms
|
||||
$(document).on('keypress', function(event) {
|
||||
var quick_row = $('.ui-page-active #quick_row_entry');
|
||||
if (quick_row.length) {
|
||||
|
||||
// if user hits enter with quick row input focused, submit form
|
||||
if (quick_row.is(':focus')) {
|
||||
if (event.which == 13) { // ENTER
|
||||
if (quick_row.val()) {
|
||||
|
@ -158,6 +159,31 @@ $(document).on('keypress', function(event) {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
} else { // quick row input not focused
|
||||
|
||||
// mimic keyboard wedge if we're so instructed
|
||||
if (quick_row.data('wedge')) {
|
||||
|
||||
if (event.which >= 48 && event.which <= 57) { // numeric (qwerty)
|
||||
if (!event.altKey && !event.ctrlKey && !event.metaKey) {
|
||||
quick_row.val(quick_row.val() + event.key);
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: these codes are correct for 'keydown' but apparently not 'keypress' ?
|
||||
// } else if (event.which >= 96 && event.which <= 105) { // numeric (10-key)
|
||||
// upc.val(upc.val() + event.key);
|
||||
|
||||
} else if (event.which == 13) { // ENTER
|
||||
// submit form when ENTER is received via keyboard "wedge"
|
||||
if (quick_row.val()) {
|
||||
var form = quick_row.parents('form:first');
|
||||
form.submit();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -18,21 +18,23 @@ ${form.render()|n}
|
|||
% if master.mobile_rows_creatable and master.rows_creatable_for(instance):
|
||||
## TODO: this seems like a poor choice of names? what are we really testing for here?
|
||||
% if master.mobile_rows_creatable_via_browse:
|
||||
${h.link_to(add_item_title, url('mobile.{}.create_row'.format(route_prefix), uuid=instance.uuid), class_='ui-btn ui-corner-all')}
|
||||
<% add_title = "Add Record" if add_item_title is Undefined else add_item_title %>
|
||||
${h.link_to(add_title, url('mobile.{}.create_row'.format(route_prefix), uuid=instance.uuid), class_='ui-btn ui-corner-all')}
|
||||
% endif
|
||||
% endif
|
||||
% if master.mobile_rows_quickable and master.rows_quickable_for(instance):
|
||||
<% placeholder = '' if quick_row_entry_placeholder is Undefined else quick_row_entry_placeholder %>
|
||||
${h.form(url('mobile.{}.quick_row'.format(route_prefix), uuid=instance.uuid))}
|
||||
${h.csrf_token(request)}
|
||||
% if quick_row_autocomplete:
|
||||
<div class="field autocomplete quick-row" data-url="${quick_row_autocomplete_url}">
|
||||
${h.hidden('quick_row_entry')}
|
||||
${h.text('quick_row_autocomplete_text', placeholder=quick_row_entry_placeholder, autocomplete='off', data_type='search')}
|
||||
${h.text('quick_row_autocomplete_text', placeholder=placeholder, autocomplete='off', data_type='search')}
|
||||
<ul data-role="listview" data-inset="true" data-filter="true" data-input="#quick_row_autocomplete_text"></ul>
|
||||
<button type="button" style="display: none;">Change</button>
|
||||
</div>
|
||||
% else:
|
||||
${h.text('quick_row_entry', placeholder=quick_row_entry_placeholder, autocomplete='off', **{'data-type': 'search', 'data-url': url('mobile.{}.quick_row'.format(route_prefix), uuid=instance.uuid)})}
|
||||
${h.text('quick_row_entry', placeholder=placeholder, autocomplete='off', **{'data-type': 'search', 'data-url': url('mobile.{}.quick_row'.format(route_prefix), uuid=instance.uuid), 'data-wedge': quick_row_keyboard_wedge})}
|
||||
% endif
|
||||
${h.end_form()}
|
||||
% endif
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/mobile/master/view.mako" />
|
||||
|
||||
<%def name="title()">Receiving » ${instance.id_str}</%def>
|
||||
|
||||
<%def name="page_title()">${h.link_to("Receiving", url('mobile.receiving'))} » ${instance.id_str}</%def>
|
||||
|
||||
${form.render()|n}
|
||||
<br />
|
||||
|
||||
% if not instance.executed and not instance.complete:
|
||||
${h.text('upc-search', class_='receiving-upc-search', placeholder="Enter UPC", autocomplete='off', **{'data-type': 'search', 'data-url': url('mobile.receiving.lookup', uuid=batch.uuid)})}
|
||||
<br />
|
||||
% endif
|
||||
|
||||
${grid.render_complete()|n}
|
||||
|
||||
% if not instance.executed and not instance.complete:
|
||||
<br /><br />
|
||||
${h.form(request.route_url('mobile.receiving.mark_complete', uuid=instance.uuid))}
|
||||
${h.csrf_token(request)}
|
||||
${h.hidden('mark-complete', value='true')}
|
||||
<button type="submit">Mark Batch as Complete</button>
|
||||
% endif
|
|
@ -2,9 +2,9 @@
|
|||
<%inherit file="/mobile/master/view_row.mako" />
|
||||
<%namespace file="/mobile/keypad.mako" import="keypad" />
|
||||
|
||||
<%def name="title()">Receiving » ${batch.id_str} » ${row.upc.pretty()}</%def>
|
||||
<%def name="title()">Receiving » ${batch.id_str} » ${master.render_product_key_value(row)}</%def>
|
||||
|
||||
<%def name="page_title()">${h.link_to("Receiving", url('mobile.receiving'))} » ${h.link_to(batch.id_str, url('mobile.receiving.view', uuid=batch.uuid))} » ${row.upc.pretty()}</%def>
|
||||
<%def name="page_title()">${h.link_to("Receiving", url('mobile.receiving'))} » ${h.link_to(batch.id_str, url('mobile.receiving.view', uuid=batch.uuid))} » ${master.render_product_key_value(row)}</%def>
|
||||
|
||||
|
||||
<div class="ui-grid-a">
|
||||
|
|
|
@ -172,10 +172,8 @@ class CustomersView(MasterView):
|
|||
|
||||
raise HTTPNotFound
|
||||
|
||||
def configure_form(self, f):
|
||||
if not self.mobile:
|
||||
super(CustomersView, self).configure_form(f)
|
||||
|
||||
def configure_common_form(self, f):
|
||||
super(CustomersView, self).configure_common_form(f)
|
||||
customer = f.model_instance
|
||||
permission_prefix = self.get_permission_prefix()
|
||||
|
||||
|
@ -222,10 +220,6 @@ class CustomersView(MasterView):
|
|||
f.set_renderer('groups', self.render_groups)
|
||||
f.set_readonly('groups')
|
||||
|
||||
def configure_mobile_form(self, f):
|
||||
super(CustomersView, self).configure_mobile_form(f)
|
||||
self.configure_form(f)
|
||||
|
||||
def objectify(self, form, data):
|
||||
customer = super(CustomersView, self).objectify(form, data)
|
||||
customer = self.objectify_contact(customer, data)
|
||||
|
|
|
@ -717,6 +717,15 @@ class MasterView(View):
|
|||
def process_uploads(self, obj, form, uploads):
|
||||
pass
|
||||
|
||||
def render_product_key_value(self, obj):
|
||||
"""
|
||||
Render the "canonical" product key value for the given object.
|
||||
"""
|
||||
product_key = self.rattail_config.product_key()
|
||||
if product_key == 'upc':
|
||||
return obj.upc.pretty() if obj.upc else ''
|
||||
return getattr(obj, product_key)
|
||||
|
||||
def before_create_flush(self, obj, form):
|
||||
pass
|
||||
|
||||
|
@ -1060,24 +1069,36 @@ class MasterView(View):
|
|||
defaults.update(kwargs)
|
||||
return defaults
|
||||
|
||||
def configure_mobile_form(self, form):
|
||||
def configure_common_form(self, form):
|
||||
"""
|
||||
Configure the primary mobile form.
|
||||
Configure the form in whatever way is deemed "common" - i.e. where
|
||||
configuration should be done the same for desktop and mobile.
|
||||
|
||||
By default this removes the 'uuid' field (if present), sets any primary
|
||||
key fields to be readonly (if we have a :attr:`model_class` and are in
|
||||
edit mode), and sets labels as defined by the master class hierarchy.
|
||||
"""
|
||||
# TODO: is any of this stuff from configure_form() needed?
|
||||
# if self.editing:
|
||||
# model_class = self.get_model_class(error=False)
|
||||
# if model_class:
|
||||
# mapper = orm.class_mapper(model_class)
|
||||
# for key in mapper.primary_key:
|
||||
# for field in form.fields:
|
||||
# if field == key.name:
|
||||
# form.set_readonly(field)
|
||||
# break
|
||||
# form.remove_field('uuid')
|
||||
form.remove_field('uuid')
|
||||
|
||||
if self.editing:
|
||||
model_class = self.get_model_class(error=False)
|
||||
if model_class:
|
||||
# set readonly for all primary key fields
|
||||
mapper = orm.class_mapper(model_class)
|
||||
for key in mapper.primary_key:
|
||||
for field in form.fields:
|
||||
if field == key.name:
|
||||
form.set_readonly(field)
|
||||
break
|
||||
|
||||
self.set_labels(form)
|
||||
|
||||
def configure_mobile_form(self, form):
|
||||
"""
|
||||
Configure the main "mobile" form for the view's data model.
|
||||
"""
|
||||
self.configure_common_form(form)
|
||||
|
||||
def validate_mobile_form(self, form):
|
||||
controls = self.request.POST.items()
|
||||
try:
|
||||
|
@ -1832,15 +1853,12 @@ class MasterView(View):
|
|||
context['row_model_title_plural'] = self.get_row_model_title_plural()
|
||||
context['row_action_url'] = self.get_row_action_url
|
||||
|
||||
if mobile:
|
||||
if mobile and self.viewing and self.mobile_rows_quickable:
|
||||
|
||||
if self.mobile_rows_creatable:
|
||||
context['add_item_title'] = "Add Record"
|
||||
# quick row does *not* mimic keyboard wedge by default, but can
|
||||
context['quick_row_keyboard_wedge'] = False
|
||||
|
||||
if self.mobile_rows_quickable:
|
||||
context['quick_row_entry_placeholder'] = "Enter search text"
|
||||
|
||||
# quick row does *not* use autocomplete by default
|
||||
# quick row does *not* use autocomplete by default, but can
|
||||
context['quick_row_autocomplete'] = False
|
||||
context['quick_row_autocomplete_url'] = '#'
|
||||
|
||||
|
@ -2366,22 +2384,9 @@ class MasterView(View):
|
|||
|
||||
def configure_form(self, form):
|
||||
"""
|
||||
Configure the primary form. By default this just sets any primary key
|
||||
fields to be readonly (if we have a :attr:`model_class`).
|
||||
Configure the main "desktop" form for the view's data model.
|
||||
"""
|
||||
if self.editing:
|
||||
model_class = self.get_model_class(error=False)
|
||||
if model_class:
|
||||
mapper = orm.class_mapper(model_class)
|
||||
for key in mapper.primary_key:
|
||||
for field in form.fields:
|
||||
if field == key.name:
|
||||
form.set_readonly(field)
|
||||
break
|
||||
|
||||
form.remove_field('uuid')
|
||||
|
||||
self.set_labels(form)
|
||||
self.configure_common_form(form)
|
||||
|
||||
def validate_form(self, form):
|
||||
if form.validate(newstyle=True):
|
||||
|
|
|
@ -139,9 +139,8 @@ class PeopleView(MasterView):
|
|||
return not bool(person.user and person.user.username == 'chuck')
|
||||
return True
|
||||
|
||||
def configure_form(self, f):
|
||||
if not self.mobile:
|
||||
super(PeopleView, self).configure_form(f)
|
||||
def configure_common_form(self, f):
|
||||
super(PeopleView, self).configure_common_form(f)
|
||||
|
||||
f.set_label('display_name', "Full Name")
|
||||
|
||||
|
@ -163,10 +162,6 @@ class PeopleView(MasterView):
|
|||
f.set_readonly('users')
|
||||
f.set_renderer('users', self.render_users)
|
||||
|
||||
def configure_mobile_form(self, f):
|
||||
super(PeopleView, self).configure_mobile_form(f)
|
||||
self.configure_form(f)
|
||||
|
||||
def render_employee(self, person, field):
|
||||
employee = person.employee
|
||||
if not employee:
|
||||
|
|
|
@ -50,6 +50,10 @@ class PurchasingBatchView(BatchMasterView):
|
|||
supports_new_product = False
|
||||
cloneable = True
|
||||
|
||||
labels = {
|
||||
'po_total': "PO Total",
|
||||
}
|
||||
|
||||
grid_columns = [
|
||||
'id',
|
||||
'vendor',
|
||||
|
@ -214,6 +218,16 @@ class PurchasingBatchView(BatchMasterView):
|
|||
# form = super(PurchasingBatchView, self).make_form(batch, **kwargs)
|
||||
# return form
|
||||
|
||||
def configure_common_form(self, f):
|
||||
super(PurchasingBatchView, self).configure_common_form(f)
|
||||
|
||||
# po_total
|
||||
if self.creating:
|
||||
f.remove_field('po_total')
|
||||
else:
|
||||
f.set_readonly('po_total')
|
||||
f.set_type('po_total', 'currency')
|
||||
|
||||
def configure_form(self, f):
|
||||
super(PurchasingBatchView, self).configure_form(f)
|
||||
batch = f.model_instance
|
||||
|
@ -329,11 +343,6 @@ class PurchasingBatchView(BatchMasterView):
|
|||
# po_number
|
||||
f.set_label('po_number', "PO Number")
|
||||
|
||||
# po_total
|
||||
f.set_readonly('po_total')
|
||||
f.set_type('po_total', 'currency')
|
||||
f.set_label('po_total', "PO Total")
|
||||
|
||||
# invoice_total
|
||||
f.set_readonly('invoice_total')
|
||||
f.set_type('invoice_total', 'currency')
|
||||
|
@ -364,12 +373,6 @@ class PurchasingBatchView(BatchMasterView):
|
|||
'vendor_contact',
|
||||
'status_code')
|
||||
|
||||
def configure_mobile_form(self, f):
|
||||
super(PurchasingBatchView, self).configure_mobile_form(f)
|
||||
|
||||
# currency fields
|
||||
f.set_type('po_total', 'currency')
|
||||
|
||||
def render_store(self, batch, field):
|
||||
store = batch.store
|
||||
if not store:
|
||||
|
|
|
@ -104,6 +104,7 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
mobile_creatable = True
|
||||
mobile_rows_filterable = True
|
||||
mobile_rows_creatable = True
|
||||
mobile_rows_quickable = True
|
||||
|
||||
allow_from_po = False
|
||||
allow_from_scratch = True
|
||||
|
@ -351,7 +352,17 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
|
||||
def get_batch_kwargs(self, batch, mobile=False):
|
||||
kwargs = super(ReceivingBatchView, self).get_batch_kwargs(batch, mobile=mobile)
|
||||
if not mobile:
|
||||
|
||||
if mobile:
|
||||
purchase = self.get_purchase(self.request.POST['purchase'])
|
||||
if isinstance(purchase, model.Purchase):
|
||||
kwargs['purchase'] = purchase
|
||||
|
||||
department = self.department_for_purchase(purchase)
|
||||
if department:
|
||||
kwargs['department'] = department
|
||||
|
||||
else: # not mobile
|
||||
batch_type = self.request.POST['batch_type']
|
||||
if batch_type == 'from_scratch':
|
||||
kwargs.pop('truck_dump_batch', None)
|
||||
|
@ -365,6 +376,9 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
raise NotImplementedError
|
||||
return kwargs
|
||||
|
||||
def department_for_purchase(self, purchase):
|
||||
pass
|
||||
|
||||
def delete_instance(self, batch):
|
||||
"""
|
||||
Delete all data (files etc.) for the batch.
|
||||
|
@ -498,6 +512,9 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
default_value=default_status)
|
||||
return filters
|
||||
|
||||
def get_purchase(self, uuid):
|
||||
return self.Session.query(model.Purchase).get(uuid)
|
||||
|
||||
def mobile_create(self):
|
||||
"""
|
||||
Mobile view for creating a new receiving batch
|
||||
|
@ -580,8 +597,9 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
f.set_readonly('invoice_total')
|
||||
|
||||
def render_mobile_row_listitem(self, row, i):
|
||||
key = self.render_product_key_value(row)
|
||||
description = row.product.full_description if row.product else row.description
|
||||
return "({}) {}".format(row.upc.pretty(), description)
|
||||
return "({}) {}".format(key, description)
|
||||
|
||||
def should_aggregate_products(self, batch):
|
||||
"""
|
||||
|
@ -590,49 +608,53 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
"""
|
||||
return True
|
||||
|
||||
# TODO: this view can create new rows, with only a GET query. that should
|
||||
# probably be changed to require POST; for now we just require the "create
|
||||
# batch row" perm and call it good..
|
||||
def mobile_lookup(self):
|
||||
"""
|
||||
Locate and/or create a row within the batch, according to the given
|
||||
product UPC, then redirect to the row view page.
|
||||
"""
|
||||
batch = self.get_instance()
|
||||
row = None
|
||||
upc = self.request.GET.get('upc', '').strip()
|
||||
upc = re.sub(r'\D', '', upc)
|
||||
if not upc:
|
||||
self.request.session.flash("Invalid UPC: {}".format(self.request.GET.get('upc')), 'error')
|
||||
return self.redirect(self.get_action_url('view', batch, mobile=True))
|
||||
|
||||
# first try to locate existing batch row by UPC match
|
||||
provided = GPC(upc, calc_check_digit=False)
|
||||
checked = GPC(upc, calc_check_digit='upc')
|
||||
rows = self.Session.query(model.PurchaseBatchRow)\
|
||||
.filter(model.PurchaseBatchRow.batch == batch)\
|
||||
.filter(model.PurchaseBatchRow.upc.in_((provided, checked)))\
|
||||
.filter(model.PurchaseBatchRow.removed == False)\
|
||||
.all()
|
||||
def quick_locate_rows(self, batch, entry):
|
||||
rows = []
|
||||
|
||||
# we prefer "exact" matches, i.e. those which assumed the entry already
|
||||
# contained the check digit.
|
||||
provided = GPC(entry, calc_check_digit=False)
|
||||
for row in batch.active_rows():
|
||||
if row.upc == provided:
|
||||
rows.append(row)
|
||||
if rows:
|
||||
if self.should_aggregate_products(batch):
|
||||
if len(rows) > 1:
|
||||
log.warning("found multiple UPC matches for {} in batch {}: {}".format(
|
||||
upc, batch.id_str, batch))
|
||||
row = rows[0]
|
||||
return rows
|
||||
|
||||
else:
|
||||
# if no "exact" matches, we'll settle for those which assume the entry
|
||||
# lacked a check digit.
|
||||
checked = GPC(entry, calc_check_digit='upc')
|
||||
for row in batch.active_rows():
|
||||
if row.upc == checked:
|
||||
rows.append(row)
|
||||
return rows
|
||||
|
||||
def save_quick_row_form(self, form):
|
||||
batch = self.get_instance()
|
||||
entry = form.validated['quick_row_entry']
|
||||
|
||||
# maybe try to locate existing row first
|
||||
rows = self.quick_locate_rows(batch, entry)
|
||||
if rows:
|
||||
|
||||
# if aggregating, just re-use matching row
|
||||
prefer_existing = self.should_aggregate_products(batch)
|
||||
if prefer_existing:
|
||||
if len(rows) > 1:
|
||||
log.warning("found multiple row matches for '%s' in batch %s: %s",
|
||||
entry, batch.id_str, batch)
|
||||
return rows[0]
|
||||
|
||||
else: # borrow product from matching row, but make new row
|
||||
other_row = rows[0]
|
||||
row = model.PurchaseBatchRow()
|
||||
row.product = other_row.product
|
||||
self.handler.add_row(batch, row)
|
||||
# TODO: is this necessary here? is so, then what about further below?
|
||||
# self.handler.refresh_batch_status(batch)
|
||||
self.Session.flush()
|
||||
return row
|
||||
|
||||
else:
|
||||
|
||||
# try to locate general product by UPC; add to batch if found
|
||||
# try to locate product by upc
|
||||
provided = GPC(entry, calc_check_digit=False)
|
||||
checked = GPC(entry, calc_check_digit='upc')
|
||||
product = api.get_product_by_upc(self.Session(), provided)
|
||||
if not product:
|
||||
product = api.get_product_by_upc(self.Session(), checked)
|
||||
|
@ -640,22 +662,25 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
row = model.PurchaseBatchRow()
|
||||
row.product = product
|
||||
self.handler.add_row(batch, row)
|
||||
self.Session.flush()
|
||||
return row
|
||||
|
||||
# check for "bad" upc
|
||||
elif len(upc) > 14:
|
||||
self.request.session.flash("Invalid UPC: {}".format(upc), 'error')
|
||||
return self.redirect(self.get_action_url('view', batch, mobile=True))
|
||||
if len(entry) > 14:
|
||||
return
|
||||
|
||||
# product in system, but sane upc, so add to batch anyway
|
||||
else:
|
||||
# product not in system, but presumably sane upc, so add to batch anyway
|
||||
row = model.PurchaseBatchRow()
|
||||
row.upc = provided # TODO: why not checked? how to know?
|
||||
row.description = "(unknown product)"
|
||||
self.handler.add_row(batch, row)
|
||||
self.handler.refresh_batch_status(batch)
|
||||
|
||||
self.Session.flush()
|
||||
return self.redirect(self.mobile_row_route_url('view', uuid=row.batch_uuid, row_uuid=row.uuid))
|
||||
return row
|
||||
|
||||
def redirect_after_quick_row(self, row, mobile=False):
|
||||
if mobile:
|
||||
return self.redirect(self.get_row_action_url('view', row, mobile=mobile))
|
||||
return super(ReceivingBatchView, self).redirect_after_quick_row(row, mobile=mobile)
|
||||
|
||||
def mobile_view_row(self):
|
||||
"""
|
||||
|
@ -806,11 +831,6 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
model_key = cls.get_model_key()
|
||||
permission_prefix = cls.get_permission_prefix()
|
||||
|
||||
# mobile lookup (note perm; this view can create new rows)
|
||||
config.add_route('mobile.{}.lookup'.format(route_prefix), '/mobile{}/{{{}}}/lookup'.format(url_prefix, model_key))
|
||||
config.add_view(cls, attr='mobile_lookup', route_name='mobile.{}.lookup'.format(route_prefix),
|
||||
renderer='json', permission='{}.create_row'.format(permission_prefix))
|
||||
|
||||
if cls.allow_truck_dump:
|
||||
config.add_route('{}.add_child_from_invoice'.format(route_prefix), '{}/{{{}}}/add-child-from-invoice'.format(url_prefix, model_key))
|
||||
config.add_view(cls, attr='add_child_from_invoice', route_name='{}.add_child_from_invoice'.format(route_prefix),
|
||||
|
|
Loading…
Reference in a new issue