Initial support for mobile ordering

plus various other changes required for that
This commit is contained in:
Lance Edgar 2017-08-02 12:08:23 -05:00
parent 5afa832684
commit 65c63dad3e
12 changed files with 289 additions and 30 deletions

View file

@ -4,6 +4,25 @@
${parent.body()} ${parent.body()}
% if master.has_rows: % if master.has_rows:
% if master.mobile_rows_creatable and not batch.executed and not batch.complete:
${h.link_to("Add Item", url('mobile.{}.create_row'.format(route_prefix), uuid=instance.uuid), class_='ui-btn ui-corner-all')}
% endif
<br /> <br />
${grid.render_complete()|n} ${grid.render_complete()|n}
% endif % endif
% if not batch.executed and request.has_perm('{}.edit'.format(permission_prefix)):
% if batch.complete:
${h.form(request.route_url('mobile.{}.mark_pending'.format(route_prefix), uuid=batch.uuid))}
${h.csrf_token(request)}
${h.hidden('mark-pending', value='true')}
${h.submit('submit', "Mark Batch as Pending")}
${h.end_form()}
% else:
${h.form(request.route_url('mobile.{}.mark_complete'.format(route_prefix), uuid=batch.uuid))}
${h.csrf_token(request)}
${h.hidden('mark-complete', value='true')}
${h.submit('submit', "Mark Batch as Complete")}
${h.end_form()}
% endif
% endif

View file

@ -0,0 +1,4 @@
## -*- coding: utf-8; -*-
<%inherit file="/mobile/master/create.mako" />
${parent.body()}

View file

@ -0,0 +1,10 @@
## -*- coding: utf-8; -*-
<%inherit file="/mobile/base.mako" />
<%def name="title()">${index_title} &raquo; ${instance_title} &raquo; Edit</%def>
<%def name="page_title()">${h.link_to(index_title, index_url)} &raquo; ${h.link_to(instance_title, instance_url)} &raquo; Edit</%def>
<div class="form-wrapper">
${form.render()|n}
</div><!-- form-wrapper -->

View file

@ -0,0 +1,16 @@
## -*- coding: utf-8; -*-
<%inherit file="/mobile/master/edit.mako" />
<%def name="title()">${index_title} &raquo; ${parent_title} &raquo; ${instance_title} &raquo; Edit</%def>
<%def name="page_title()">${h.link_to(index_title, index_url)} &raquo; ${h.link_to(parent_title, parent_url)} &raquo; ${h.link_to(instance_title, instance_url)} &raquo; Edit</%def>
<%def name="buttons()">
<br />
${h.submit('create', form.update_label)}
<a href="${form.cancel_url}" class="ui-btn">Cancel</a>
</%def>
<div class="form-wrapper">
${form.render(buttons=capture(self.buttons))|n}
</div><!-- form-wrapper -->

View file

@ -1,4 +1,12 @@
## -*- coding: utf-8; -*- ## -*- coding: utf-8; -*-
<%inherit file="/mobile/master/view.mako" /> <%inherit file="/mobile/master/view.mako" />
<%def name="title()">${index_title} &raquo; ${parent_title} &raquo; ${instance_title}</%def>
<%def name="page_title()">${h.link_to(index_title, index_url)} &raquo; ${h.link_to(parent_title, parent_url)} &raquo; ${instance_title}</%def>
${parent.body()} ${parent.body()}
% if master.mobile_rows_editable and instance_editable and request.has_perm('{}.edit_row'.format(permission_prefix)):
${h.link_to("Edit", url('mobile.{}.edit'.format(row_route_prefix), uuid=instance.uuid), class_='ui-btn')}
% endif

View file

@ -0,0 +1,22 @@
## -*- coding: utf-8; -*-
<%inherit file="/mobile/base.mako" />
<%def name="title()">${index_title} &raquo; New Batch</%def>
<%def name="page_title()">${h.link_to(index_title, index_url)} &raquo; New Batch</%def>
${h.form(request.current_route_url(), class_='ui-filterable', name='new-purchasing-batch')}
${h.csrf_token(request)}
<div class="field-wrapper vendor">
<div class="field autocomplete" data-url="${url('vendors.autocomplete')}">
${h.hidden('vendor')}
${h.text('new-purchasing-batch-vendor-text', placeholder="Vendor name", autocomplete='off', data_type='search')}
<ul data-role="listview" data-inset="true" data-filter="true" data-input="#new-purchasing-batch-vendor-text"></ul>
<button type="button" style="display: none;">Change Vendor</button>
</div>
</div>
<br />
${h.submit('submit', "Make Batch")}
${h.end_form()}

View file

@ -0,0 +1,6 @@
## -*- coding: utf-8; -*-
<%inherit file="/mobile/master/create_row.mako" />
<%def name="title()">${h.link_to(index_title, index_url)} &raquo; ${h.link_to(instance_title, instance_url)} &raquo; Add Item</%def>
${parent.body()}

View file

@ -40,7 +40,10 @@
<span class="global">${index_title}</span> <span class="global">${index_title}</span>
% else: % else:
${h.link_to(index_title, index_url, class_='global')} ${h.link_to(index_title, index_url, class_='global')}
% if instance_url is not Undefined: % if parent_url is not Undefined:
<span class="global">&raquo;</span>
${h.link_to(parent_title, parent_url, class_='global')}
% elif instance_url is not Undefined:
<span class="global">&raquo;</span> <span class="global">&raquo;</span>
${h.link_to(instance_title, instance_url, class_='global')} ${h.link_to(instance_title, instance_url, class_='global')}
% endif % endif

View file

@ -354,6 +354,11 @@ class BatchMasterView(MasterView):
batch.complete = True batch.complete = True
return self.redirect(self.get_index_url(mobile=True)) return self.redirect(self.get_index_url(mobile=True))
def mobile_mark_pending(self):
batch = self.get_instance()
batch.complete = False
return self.redirect(self.get_action_url('view', batch, mobile=True))
def rows_creatable_for(self, batch): def rows_creatable_for(self, batch):
""" """
Only allow creating new rows on a batch if it hasn't yet been executed. Only allow creating new rows on a batch if it hasn't yet been executed.
@ -370,6 +375,24 @@ class BatchMasterView(MasterView):
return self.redirect(self.get_action_url('view', batch)) return self.redirect(self.get_action_url('view', batch))
return super(BatchMasterView, self).create_row() return super(BatchMasterView, self).create_row()
def mobile_create_row(self):
"""
Only allow creating a new row if the batch hasn't yet been executed.
"""
batch = self.get_instance()
if batch.executed:
self.request.session.flash("You cannot add new rows to a batch which has been executed")
return self.redirect(self.get_action_url('view', batch, mobile=True))
return super(BatchMasterView, self).mobile_create_row()
def before_create_row(self, form):
batch = self.get_instance()
row = form.fieldset.model
self.handler.add_row(batch, row)
def after_create_row(self, row):
self.handler.refresh_row(row)
def make_default_row_grid_tools(self, batch): def make_default_row_grid_tools(self, batch):
if self.rows_creatable and not batch.executed: if self.rows_creatable and not batch.executed:
permission_prefix = self.get_permission_prefix() permission_prefix = self.get_permission_prefix()
@ -659,7 +682,8 @@ class BatchMasterView(MasterView):
""" """
Batch rows are editable only until batch has been executed. Batch rows are editable only until batch has been executed.
""" """
return self.rows_editable and not row.batch.executed batch = row.batch
return self.rows_editable and not batch.executed and not batch.complete
def row_edit_action_url(self, row, i): def row_edit_action_url(self, row, i):
if self.row_editable(row): if self.row_editable(row):
@ -989,6 +1013,11 @@ class BatchMasterView(MasterView):
config.add_view(cls, attr='mobile_mark_complete', route_name='mobile.{}.mark_complete'.format(route_prefix), config.add_view(cls, attr='mobile_mark_complete', route_name='mobile.{}.mark_complete'.format(route_prefix),
permission='{}.edit'.format(permission_prefix)) permission='{}.edit'.format(permission_prefix))
# mobile mark pending
config.add_route('mobile.{}.mark_pending'.format(route_prefix), '/mobile{}/{{{}}}/mark-pending'.format(url_prefix, model_key))
config.add_view(cls, attr='mobile_mark_pending', route_name='mobile.{}.mark_pending'.format(route_prefix),
permission='{}.edit'.format(permission_prefix))
# execute batch # execute batch
config.add_route('{}.execute'.format(route_prefix), '{}/{{uuid}}/execute'.format(url_prefix)) config.add_route('{}.execute'.format(route_prefix), '{}/{{uuid}}/execute'.format(url_prefix))

View file

@ -95,8 +95,10 @@ class MasterView(View):
rows_bulk_deletable = False rows_bulk_deletable = False
rows_default_pagesize = 20 rows_default_pagesize = 20
mobile_rows_creatable = False
mobile_rows_filterable = False mobile_rows_filterable = False
mobile_rows_viewable = False mobile_rows_viewable = False
mobile_rows_editable = False
@property @property
def Session(self): def Session(self):
@ -472,7 +474,12 @@ class MasterView(View):
fieldset = self.make_fieldset(row) fieldset = self.make_fieldset(row)
self.preconfigure_mobile_row_fieldset(fieldset) self.preconfigure_mobile_row_fieldset(fieldset)
self.configure_mobile_row_fieldset(fieldset) self.configure_mobile_row_fieldset(fieldset)
kwargs.setdefault('session', self.Session())
kwargs.setdefault('creating', self.creating)
kwargs.setdefault('editing', self.editing)
kwargs.setdefault('action_url', self.request.current_route_url(_query=None)) kwargs.setdefault('action_url', self.request.current_route_url(_query=None))
if 'cancel_url' not in kwargs:
kwargs['cancel_url'] = self.get_action_url('view', self.get_parent(row), mobile=True)
factory = kwargs.pop('factory', forms.AlchemyForm) factory = kwargs.pop('factory', forms.AlchemyForm)
form = factory(self.request, fieldset, **kwargs) form = factory(self.request, fieldset, **kwargs)
form.readonly = self.viewing form.readonly = self.viewing
@ -508,11 +515,16 @@ class MasterView(View):
""" """
self.viewing = True self.viewing = True
row = self.get_row_instance() row = self.get_row_instance()
parent = self.get_parent(row)
form = self.make_mobile_row_form(row) form = self.make_mobile_row_form(row)
context = { context = {
'row': row, 'row': row,
'parent_instance': parent,
'parent_title': self.get_instance_title(parent),
'parent_url': self.get_action_url('view', parent, mobile=True),
'instance': row, 'instance': row,
'instance_title': self.get_row_instance_title(row), 'instance_title': self.get_row_instance_title(row),
'instance_editable': self.row_editable(row),
'parent_model_title': self.get_model_title(), 'parent_model_title': self.get_model_title(),
'form': form, 'form': form,
} }
@ -945,6 +957,8 @@ class MasterView(View):
context.update(self.template_kwargs(**context)) context.update(self.template_kwargs(**context))
if hasattr(self, 'template_kwargs_{}'.format(template)): if hasattr(self, 'template_kwargs_{}'.format(template)):
context.update(getattr(self, 'template_kwargs_{}'.format(template))(**context)) context.update(getattr(self, 'template_kwargs_{}'.format(template))(**context))
if hasattr(self, 'mobile_template_kwargs_{}'.format(template)):
context.update(getattr(self, 'mobile_template_kwargs_{}'.format(template))(**context))
# First try the template path most specific to the view. # First try the template path most specific to the view.
if mobile: if mobile:
@ -1327,8 +1341,30 @@ class MasterView(View):
def after_create_row(self, row_object): def after_create_row(self, row_object):
pass pass
def redirect_after_create_row(self, row): def redirect_after_create_row(self, row, mobile=False):
return self.redirect(self.get_action_url('view', self.get_parent(row))) return self.redirect(self.get_row_action_url('view', row, mobile=mobile))
def mobile_create_row(self):
"""
Mobile view for creating a new row object
"""
self.creating = True
parent = self.get_instance()
instance_url = self.get_action_url('view', parent, mobile=True)
form = self.make_mobile_row_form(self.model_row_class, cancel_url=instance_url)
if self.request.method == 'POST':
if form.validate():
self.before_create_row(form)
# let save() return alternate object if necessary
obj = self.save_create_row_form(form) or form.fieldset.model
self.after_create_row(obj)
return self.redirect_after_create_row(obj, mobile=True)
return self.render_to_response('create_row', {
'instance_title': self.get_instance_title(parent),
'instance_url': instance_url,
'parent_object': parent,
'form': form,
}, mobile=True)
def view_row(self): def view_row(self):
""" """
@ -1359,6 +1395,14 @@ class MasterView(View):
""" """
return True return True
def row_editable(self, row):
"""
Returns boolean indicating whether or not the given row can be
considered "editable". Returns ``True`` by default; override as
necessary.
"""
return True
def edit_row(self): def edit_row(self):
""" """
View for editing an existing model record. View for editing an existing model record.
@ -1376,14 +1420,39 @@ class MasterView(View):
return self.render_to_response('edit_row', { return self.render_to_response('edit_row', {
'instance': row, 'instance': row,
'row_parent': parent, 'row_parent': parent,
'parent_title': self.get_instance_title(parent),
'parent_url': self.get_action_url('view', parent),
'parent_instance': parent,
'instance_title': self.get_row_instance_title(row), 'instance_title': self.get_row_instance_title(row),
'instance_deletable': self.row_deletable(row), 'instance_deletable': self.row_deletable(row),
'index_url': self.get_action_url('view', parent),
'index_title': '{} {}'.format(
self.get_model_title(),
self.get_instance_title(parent)),
'form': form}) 'form': form})
def mobile_edit_row(self):
"""
Mobile view for editing a row object
"""
self.editing = True
row = self.get_row_instance()
instance_url = self.get_row_action_url('view', row, mobile=True)
form = self.make_mobile_row_form(row)
if self.request.method == 'POST':
if form.validate():
self.save_edit_row_form(form)
return self.redirect_after_edit_row(row, mobile=True)
parent = self.get_parent(row)
return self.render_to_response('edit_row', {
'instance': row,
'instance_title': self.get_row_instance_title(row),
'instance_url': instance_url,
'instance_deletable': self.row_deletable(row),
'parent_instance': parent,
'parent_title': self.get_instance_title(parent),
'parent_url': self.get_action_url('view', parent, mobile=True),
'form': form},
mobile=True)
def save_edit_row_form(self, form): def save_edit_row_form(self, form):
self.save_row_form(form) self.save_row_form(form)
self.after_edit_row(form.fieldset.model) self.after_edit_row(form.fieldset.model)
@ -1396,16 +1465,8 @@ class MasterView(View):
Event hook, called just after an existing row object is saved. Event hook, called just after an existing row object is saved.
""" """
def redirect_after_edit_row(self, row): def redirect_after_edit_row(self, row, mobile=False):
return self.redirect(self.get_action_url('view', self.get_parent(row))) return self.redirect(self.get_row_action_url('view', row, mobile=True))
def row_editable(self, row):
"""
Returns boolean indicating whether or not the given row can be
considered "editable". Returns ``True`` by default; override as
necessary.
"""
return True
def row_deletable(self, row): def row_deletable(self, row):
""" """
@ -1617,12 +1678,18 @@ class MasterView(View):
### sub-rows stuff follows ### sub-rows stuff follows
# create row # create row
if cls.has_rows and cls.rows_creatable: if cls.has_rows:
if cls.rows_creatable or cls.mobile_rows_creatable:
config.add_tailbone_permission(permission_prefix, '{}.create_row'.format(permission_prefix),
"Create new {} rows".format(model_title))
if cls.rows_creatable:
config.add_route('{}.create_row'.format(route_prefix), '{}/{{{}}}/new-row'.format(url_prefix, model_key)) config.add_route('{}.create_row'.format(route_prefix), '{}/{{{}}}/new-row'.format(url_prefix, model_key))
config.add_view(cls, attr='create_row', route_name='{}.create_row'.format(route_prefix), config.add_view(cls, attr='create_row', route_name='{}.create_row'.format(route_prefix),
permission='{}.create_row'.format(permission_prefix)) permission='{}.create_row'.format(permission_prefix))
config.add_tailbone_permission(permission_prefix, '{}.create_row'.format(permission_prefix), if cls.mobile_rows_creatable:
"Create new {} rows".format(model_title)) config.add_route('mobile.{}.create_row'.format(route_prefix), '/mobile{}/{{{}}}/new-row'.format(url_prefix, model_key))
config.add_view(cls, attr='mobile_create_row', route_name='mobile.{}.create_row'.format(route_prefix),
permission='{}.create_row'.format(permission_prefix))
# view row # view row
if cls.has_rows: if cls.has_rows:
@ -1636,12 +1703,18 @@ class MasterView(View):
permission='{}.view'.format(permission_prefix)) permission='{}.view'.format(permission_prefix))
# edit row # edit row
if cls.has_rows and cls.rows_editable: if cls.has_rows:
if cls.rows_editable or cls.mobile_rows_editable:
config.add_tailbone_permission(permission_prefix, '{}.edit_row'.format(permission_prefix),
"Edit individual {} rows".format(model_title))
if cls.rows_editable:
config.add_route('{}.edit'.format(row_route_prefix), '{}/{{uuid}}/edit'.format(row_url_prefix)) config.add_route('{}.edit'.format(row_route_prefix), '{}/{{uuid}}/edit'.format(row_url_prefix))
config.add_view(cls, attr='edit_row', route_name='{}.edit'.format(row_route_prefix), config.add_view(cls, attr='edit_row', route_name='{}.edit'.format(row_route_prefix),
permission='{}.edit_row'.format(permission_prefix)) permission='{}.edit_row'.format(permission_prefix))
config.add_tailbone_permission(permission_prefix, '{}.edit_row'.format(permission_prefix), if cls.mobile_rows_editable:
"Edit individual {} rows".format(model_title)) config.add_route('mobile.{}.edit'.format(row_route_prefix), '/mobile{}/{{uuid}}/edit'.format(row_url_prefix))
config.add_view(cls, attr='mobile_edit_row', route_name='mobile.{}.edit'.format(row_route_prefix),
permission='{}.edit_row'.format(permission_prefix))
# delete row # delete row
if cls.has_rows and cls.rows_deletable: if cls.has_rows and cls.rows_deletable:

View file

@ -275,6 +275,7 @@ class PurchasingBatchView(BatchMasterView):
elif batch.buyer_uuid: elif batch.buyer_uuid:
kwargs['buyer_uuid'] = batch.buyer_uuid kwargs['buyer_uuid'] = batch.buyer_uuid
kwargs['po_number'] = batch.po_number kwargs['po_number'] = batch.po_number
kwargs['po_total'] = batch.po_total
# TODO: should these always get set? # TODO: should these always get set?
if self.batch_mode == self.enum.PURCHASE_BATCH_MODE_ORDERING: if self.batch_mode == self.enum.PURCHASE_BATCH_MODE_ORDERING:
@ -352,6 +353,7 @@ class PurchasingBatchView(BatchMasterView):
def _preconfigure_row_fieldset(self, fs): def _preconfigure_row_fieldset(self, fs):
super(PurchasingBatchView, self)._preconfigure_row_fieldset(fs) super(PurchasingBatchView, self)._preconfigure_row_fieldset(fs)
fs.upc.set(label="UPC") fs.upc.set(label="UPC")
fs.item_id.set(label="Item ID")
fs.brand_name.set(label="Brand") fs.brand_name.set(label="Brand")
fs.case_quantity.set(renderer=forms.renderers.QuantityFieldRenderer, readonly=True) fs.case_quantity.set(renderer=forms.renderers.QuantityFieldRenderer, readonly=True)
fs.cases_ordered.set(renderer=forms.renderers.QuantityFieldRenderer) fs.cases_ordered.set(renderer=forms.renderers.QuantityFieldRenderer)
@ -402,6 +404,7 @@ class PurchasingBatchView(BatchMasterView):
include=[ include=[
# fs.item_lookup, # fs.item_lookup,
fs.upc, fs.upc,
fs.item_id,
fs.product, fs.product,
fs.brand_name, fs.brand_name,
fs.description, fs.description,

View file

@ -51,6 +51,9 @@ class OrderingBatchView(PurchasingBatchView):
model_title = "Ordering Batch" model_title = "Ordering Batch"
model_title_plural = "Ordering Batches" model_title_plural = "Ordering Batches"
index_title = "Ordering" index_title = "Ordering"
mobile_creatable = True
rows_editable = True
mobile_rows_editable = True
row_grid_columns = [ row_grid_columns = [
'sequence', 'sequence',
@ -241,6 +244,69 @@ class OrderingBatchView(PurchasingBatchView):
'batch_po_total': '${:0,.2f}'.format(batch.po_total or 0), 'batch_po_total': '${:0,.2f}'.format(batch.po_total or 0),
} }
def render_mobile_listitem(self, batch, i):
return "({}) {} on {} for ${:0,.2f}".format(batch.id_str, batch.vendor,
batch.date_ordered, batch.po_total)
def mobile_create(self):
"""
Mobile view for creating a new ordering batch
"""
mode = self.batch_mode
data = {'mode': mode}
vendor = None
if self.request.method == 'POST' and self.request.POST.get('vendor'):
vendor = self.Session.query(model.Vendor).get(self.request.POST['vendor'])
if vendor:
# fetch first to avoid flush below
store = self.rattail_config.get_store(self.Session())
batch = self.model_class()
batch.mode = mode
batch.vendor = vendor
batch.store = store
batch.buyer = self.request.user.employee
batch.date_ordered = localtime(self.rattail_config).date()
batch.created_by = self.request.user
batch.po_total = 0
kwargs = self.get_batch_kwargs(batch, mobile=True)
batch = self.handler.make_batch(self.Session(), **kwargs)
if self.handler.should_populate(batch):
self.handler.populate(batch)
return self.redirect(self.request.route_url('mobile.ordering.view', uuid=batch.uuid))
data['index_title'] = self.get_index_title()
data['index_url'] = self.get_index_url(mobile=True)
data['mode_title'] = self.enum.PURCHASE_BATCH_MODE[mode].capitalize()
return self.render_to_response('create', data, mobile=True)
def preconfigure_mobile_fieldset(self, fs):
super(OrderingBatchView, self).preconfigure_mobile_fieldset(fs)
fs.vendor.set(attrs={'hyperlink': False})
def configure_mobile_fieldset(self, fs):
fields = [
fs.vendor,
fs.department,
fs.date_ordered,
fs.po_number,
fs.po_total,
fs.created,
fs.created_by,
fs.notes,
fs.status_code,
fs.complete,
]
batch = fs.model
if (self.viewing or self.deleting) and batch.executed:
fields.extend([
fs.executed,
fs.executed_by,
])
fs.configure(include=fields)
def download_excel(self): def download_excel(self):
""" """
Download ordering batch as Excel spreadsheet. Download ordering batch as Excel spreadsheet.