From 65c63dad3edb8663cfa75b15f7967bfdcb2ebecb Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Wed, 2 Aug 2017 12:08:23 -0500 Subject: [PATCH] Initial support for mobile ordering plus various other changes required for that --- tailbone/templates/mobile/batch/view.mako | 19 +++ .../templates/mobile/master/create_row.mako | 4 + tailbone/templates/mobile/master/edit.mako | 10 ++ .../templates/mobile/master/edit_row.mako | 16 +++ .../templates/mobile/master/view_row.mako | 8 ++ .../templates/mobile/ordering/create.mako | 22 +++ .../templates/mobile/ordering/create_row.mako | 6 + tailbone/templates/themes/better/base.mako | 5 +- tailbone/views/batch/core.py | 31 ++++- tailbone/views/master.py | 129 ++++++++++++++---- tailbone/views/purchasing/batch.py | 3 + tailbone/views/purchasing/ordering.py | 66 +++++++++ 12 files changed, 289 insertions(+), 30 deletions(-) create mode 100644 tailbone/templates/mobile/master/create_row.mako create mode 100644 tailbone/templates/mobile/master/edit.mako create mode 100644 tailbone/templates/mobile/master/edit_row.mako create mode 100644 tailbone/templates/mobile/ordering/create.mako create mode 100644 tailbone/templates/mobile/ordering/create_row.mako diff --git a/tailbone/templates/mobile/batch/view.mako b/tailbone/templates/mobile/batch/view.mako index 9d3b0c0c..0770e703 100644 --- a/tailbone/templates/mobile/batch/view.mako +++ b/tailbone/templates/mobile/batch/view.mako @@ -4,6 +4,25 @@ ${parent.body()} % 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
${grid.render_complete()|n} % 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 diff --git a/tailbone/templates/mobile/master/create_row.mako b/tailbone/templates/mobile/master/create_row.mako new file mode 100644 index 00000000..8a6157d2 --- /dev/null +++ b/tailbone/templates/mobile/master/create_row.mako @@ -0,0 +1,4 @@ +## -*- coding: utf-8; -*- +<%inherit file="/mobile/master/create.mako" /> + +${parent.body()} diff --git a/tailbone/templates/mobile/master/edit.mako b/tailbone/templates/mobile/master/edit.mako new file mode 100644 index 00000000..3c13a8e4 --- /dev/null +++ b/tailbone/templates/mobile/master/edit.mako @@ -0,0 +1,10 @@ +## -*- coding: utf-8; -*- +<%inherit file="/mobile/base.mako" /> + +<%def name="title()">${index_title} » ${instance_title} » Edit + +<%def name="page_title()">${h.link_to(index_title, index_url)} » ${h.link_to(instance_title, instance_url)} » Edit + +
+ ${form.render()|n} +
diff --git a/tailbone/templates/mobile/master/edit_row.mako b/tailbone/templates/mobile/master/edit_row.mako new file mode 100644 index 00000000..31b0156d --- /dev/null +++ b/tailbone/templates/mobile/master/edit_row.mako @@ -0,0 +1,16 @@ +## -*- coding: utf-8; -*- +<%inherit file="/mobile/master/edit.mako" /> + +<%def name="title()">${index_title} » ${parent_title} » ${instance_title} » Edit + +<%def name="page_title()">${h.link_to(index_title, index_url)} » ${h.link_to(parent_title, parent_url)} » ${h.link_to(instance_title, instance_url)} » Edit + +<%def name="buttons()"> +
+ ${h.submit('create', form.update_label)} + Cancel + + +
+ ${form.render(buttons=capture(self.buttons))|n} +
diff --git a/tailbone/templates/mobile/master/view_row.mako b/tailbone/templates/mobile/master/view_row.mako index 74df7e9e..109ad415 100644 --- a/tailbone/templates/mobile/master/view_row.mako +++ b/tailbone/templates/mobile/master/view_row.mako @@ -1,4 +1,12 @@ ## -*- coding: utf-8; -*- <%inherit file="/mobile/master/view.mako" /> +<%def name="title()">${index_title} » ${parent_title} » ${instance_title} + +<%def name="page_title()">${h.link_to(index_title, index_url)} » ${h.link_to(parent_title, parent_url)} » ${instance_title} + ${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 diff --git a/tailbone/templates/mobile/ordering/create.mako b/tailbone/templates/mobile/ordering/create.mako new file mode 100644 index 00000000..68f11737 --- /dev/null +++ b/tailbone/templates/mobile/ordering/create.mako @@ -0,0 +1,22 @@ +## -*- coding: utf-8; -*- +<%inherit file="/mobile/base.mako" /> + +<%def name="title()">${index_title} » New Batch + +<%def name="page_title()">${h.link_to(index_title, index_url)} » New Batch + +${h.form(request.current_route_url(), class_='ui-filterable', name='new-purchasing-batch')} +${h.csrf_token(request)} + +
+
+ ${h.hidden('vendor')} + ${h.text('new-purchasing-batch-vendor-text', placeholder="Vendor name", autocomplete='off', data_type='search')} +
    + +
    +
    + +
    +${h.submit('submit', "Make Batch")} +${h.end_form()} diff --git a/tailbone/templates/mobile/ordering/create_row.mako b/tailbone/templates/mobile/ordering/create_row.mako new file mode 100644 index 00000000..d31814f8 --- /dev/null +++ b/tailbone/templates/mobile/ordering/create_row.mako @@ -0,0 +1,6 @@ +## -*- coding: utf-8; -*- +<%inherit file="/mobile/master/create_row.mako" /> + +<%def name="title()">${h.link_to(index_title, index_url)} » ${h.link_to(instance_title, instance_url)} » Add Item + +${parent.body()} diff --git a/tailbone/templates/themes/better/base.mako b/tailbone/templates/themes/better/base.mako index 1c7d55e4..76710c34 100644 --- a/tailbone/templates/themes/better/base.mako +++ b/tailbone/templates/themes/better/base.mako @@ -40,7 +40,10 @@ ${index_title} % else: ${h.link_to(index_title, index_url, class_='global')} - % if instance_url is not Undefined: + % if parent_url is not Undefined: + » + ${h.link_to(parent_title, parent_url, class_='global')} + % elif instance_url is not Undefined: » ${h.link_to(instance_title, instance_url, class_='global')} % endif diff --git a/tailbone/views/batch/core.py b/tailbone/views/batch/core.py index dfccc047..fc9ef4c9 100644 --- a/tailbone/views/batch/core.py +++ b/tailbone/views/batch/core.py @@ -354,6 +354,11 @@ class BatchMasterView(MasterView): batch.complete = 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): """ 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 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): if self.rows_creatable and not batch.executed: permission_prefix = self.get_permission_prefix() @@ -659,7 +682,8 @@ class BatchMasterView(MasterView): """ 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): 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), 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 config.add_route('{}.execute'.format(route_prefix), '{}/{{uuid}}/execute'.format(url_prefix)) diff --git a/tailbone/views/master.py b/tailbone/views/master.py index d3e30b9e..3c59e2fe 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -95,8 +95,10 @@ class MasterView(View): rows_bulk_deletable = False rows_default_pagesize = 20 + mobile_rows_creatable = False mobile_rows_filterable = False mobile_rows_viewable = False + mobile_rows_editable = False @property def Session(self): @@ -472,7 +474,12 @@ class MasterView(View): fieldset = self.make_fieldset(row) self.preconfigure_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)) + 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) form = factory(self.request, fieldset, **kwargs) form.readonly = self.viewing @@ -508,11 +515,16 @@ class MasterView(View): """ self.viewing = True row = self.get_row_instance() + parent = self.get_parent(row) form = self.make_mobile_row_form(row) context = { '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_title': self.get_row_instance_title(row), + 'instance_editable': self.row_editable(row), 'parent_model_title': self.get_model_title(), 'form': form, } @@ -945,6 +957,8 @@ class MasterView(View): context.update(self.template_kwargs(**context)) if hasattr(self, 'template_kwargs_{}'.format(template)): 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. if mobile: @@ -1327,8 +1341,30 @@ class MasterView(View): def after_create_row(self, row_object): pass - def redirect_after_create_row(self, row): - return self.redirect(self.get_action_url('view', self.get_parent(row))) + def redirect_after_create_row(self, row, mobile=False): + 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): """ @@ -1359,6 +1395,14 @@ class MasterView(View): """ 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): """ View for editing an existing model record. @@ -1376,14 +1420,39 @@ class MasterView(View): return self.render_to_response('edit_row', { 'instance': row, '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_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}) + 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): self.save_row_form(form) 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. """ - def redirect_after_edit_row(self, row): - return self.redirect(self.get_action_url('view', self.get_parent(row))) - - 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 redirect_after_edit_row(self, row, mobile=False): + return self.redirect(self.get_row_action_url('view', row, mobile=True)) def row_deletable(self, row): """ @@ -1617,12 +1678,18 @@ class MasterView(View): ### sub-rows stuff follows # create row - if cls.has_rows and cls.rows_creatable: - 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), - permission='{}.create_row'.format(permission_prefix)) - config.add_tailbone_permission(permission_prefix, '{}.create_row'.format(permission_prefix), - "Create new {} rows".format(model_title)) + 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_view(cls, attr='create_row', route_name='{}.create_row'.format(route_prefix), + permission='{}.create_row'.format(permission_prefix)) + if cls.mobile_rows_creatable: + 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 if cls.has_rows: @@ -1636,12 +1703,18 @@ class MasterView(View): permission='{}.view'.format(permission_prefix)) # edit row - if cls.has_rows and cls.rows_editable: - 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), - permission='{}.edit_row'.format(permission_prefix)) - config.add_tailbone_permission(permission_prefix, '{}.edit_row'.format(permission_prefix), - "Edit individual {} rows".format(model_title)) + 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_view(cls, attr='edit_row', route_name='{}.edit'.format(row_route_prefix), + permission='{}.edit_row'.format(permission_prefix)) + if cls.mobile_rows_editable: + 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 if cls.has_rows and cls.rows_deletable: diff --git a/tailbone/views/purchasing/batch.py b/tailbone/views/purchasing/batch.py index ca26ea05..738e175e 100644 --- a/tailbone/views/purchasing/batch.py +++ b/tailbone/views/purchasing/batch.py @@ -275,6 +275,7 @@ class PurchasingBatchView(BatchMasterView): elif batch.buyer_uuid: kwargs['buyer_uuid'] = batch.buyer_uuid kwargs['po_number'] = batch.po_number + kwargs['po_total'] = batch.po_total # TODO: should these always get set? if self.batch_mode == self.enum.PURCHASE_BATCH_MODE_ORDERING: @@ -352,6 +353,7 @@ class PurchasingBatchView(BatchMasterView): def _preconfigure_row_fieldset(self, fs): super(PurchasingBatchView, self)._preconfigure_row_fieldset(fs) fs.upc.set(label="UPC") + fs.item_id.set(label="Item ID") fs.brand_name.set(label="Brand") fs.case_quantity.set(renderer=forms.renderers.QuantityFieldRenderer, readonly=True) fs.cases_ordered.set(renderer=forms.renderers.QuantityFieldRenderer) @@ -402,6 +404,7 @@ class PurchasingBatchView(BatchMasterView): include=[ # fs.item_lookup, fs.upc, + fs.item_id, fs.product, fs.brand_name, fs.description, diff --git a/tailbone/views/purchasing/ordering.py b/tailbone/views/purchasing/ordering.py index 7f16c9b6..514fdb62 100644 --- a/tailbone/views/purchasing/ordering.py +++ b/tailbone/views/purchasing/ordering.py @@ -51,6 +51,9 @@ class OrderingBatchView(PurchasingBatchView): model_title = "Ordering Batch" model_title_plural = "Ordering Batches" index_title = "Ordering" + mobile_creatable = True + rows_editable = True + mobile_rows_editable = True row_grid_columns = [ 'sequence', @@ -241,6 +244,69 @@ class OrderingBatchView(PurchasingBatchView): '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): """ Download ordering batch as Excel spreadsheet.