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
9 changed files with 177 additions and 156 deletions
|
@ -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,17 +1853,14 @@ 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
|
||||
context['quick_row_autocomplete'] = False
|
||||
context['quick_row_autocomplete_url'] = '#'
|
||||
# quick row does *not* use autocomplete by default, but can
|
||||
context['quick_row_autocomplete'] = False
|
||||
context['quick_row_autocomplete_url'] = '#'
|
||||
|
||||
context.update(data)
|
||||
context.update(self.template_kwargs(**context))
|
||||
|
@ -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,72 +608,79 @@ 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 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)
|
||||
if product:
|
||||
row = model.PurchaseBatchRow()
|
||||
row.product = product
|
||||
self.handler.add_row(batch, row)
|
||||
self.Session.flush()
|
||||
return row
|
||||
|
||||
# try to locate general product by UPC; add to batch if found
|
||||
product = api.get_product_by_upc(self.Session(), provided)
|
||||
if not product:
|
||||
product = api.get_product_by_upc(self.Session(), checked)
|
||||
if product:
|
||||
row = model.PurchaseBatchRow()
|
||||
row.product = product
|
||||
self.handler.add_row(batch, 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))
|
||||
|
||||
# product in system, but sane upc, so add to batch anyway
|
||||
else:
|
||||
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)
|
||||
# check for "bad" upc
|
||||
if len(entry) > 14:
|
||||
return
|
||||
|
||||
# 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.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…
Add table
Add a link
Reference in a new issue