diff --git a/tailbone/templates/batch/handheld/exec_options.mako b/tailbone/templates/batch/handheld/exec_options.mako
new file mode 100644
index 00000000..af5eaf14
--- /dev/null
+++ b/tailbone/templates/batch/handheld/exec_options.mako
@@ -0,0 +1,3 @@
+## -*- coding: utf-8 -*-
+
+${form.field_div('action', form.select('action', options=ACTION_OPTIONS), label="Action to Perform")}
diff --git a/tailbone/views/handheld.py b/tailbone/views/handheld.py
new file mode 100644
index 00000000..9d0134c6
--- /dev/null
+++ b/tailbone/views/handheld.py
@@ -0,0 +1,153 @@
+# -*- coding: utf-8 -*-
+################################################################################
+#
+# Rattail -- Retail Software Framework
+# Copyright © 2010-2016 Lance Edgar
+#
+# This file is part of Rattail.
+#
+# Rattail is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
+# more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Rattail. If not, see .
+#
+################################################################################
+"""
+Views for handheld batches
+"""
+
+from __future__ import unicode_literals, absolute_import
+
+from rattail.db.batch.handheld.handler import HandheldBatchHandler
+from rattail.util import OrderedDict
+
+import formalchemy as fa
+import formencode as fe
+from webhelpers.html import tags
+
+from tailbone import forms
+from tailbone.views.batch import FileBatchMasterView
+
+from dtail import enum
+from dtail.db import model
+
+
+ACTION_OPTIONS = OrderedDict([
+ ('make_label_batch', "Make a new Label Batch"),
+ ('make_inventory_batch', "Make a new Inventory Batch"),
+])
+
+
+class ExecutionOptions(fe.Schema):
+ allow_extra_fields = True
+ filter_extra_fields = True
+ action = fe.validators.OneOf(ACTION_OPTIONS)
+
+
+class InventoryBatchFieldRenderer(fa.FieldRenderer):
+ """
+ Renderer for handheld batch's "inventory batch" field.
+ """
+
+ def render_readonly(self, **kwargs):
+ batch = self.raw_value
+ if batch:
+ return tags.link_to(
+ batch.id_str,
+ self.request.route_url('batch.inventory.view', uuid=batch.uuid))
+ return ''
+
+
+
+class HandheldBatchView(FileBatchMasterView):
+ """
+ Master view for handheld batches.
+ """
+ model_class = model.HandheldBatch
+ model_title_plural = "Handheld Batches"
+ batch_row_class = model.HandheldBatchRow
+ batch_handler_class = HandheldBatchHandler
+ route_prefix = 'batch.handheld'
+ url_prefix = '/batch/handheld'
+ execution_options_schema = ExecutionOptions
+
+ def configure_grid(self, g):
+ g.configure(
+ include=[
+ g.id,
+ g.created,
+ g.created_by,
+ g.device_name,
+ g.executed,
+ g.executed_by,
+ ],
+ readonly=True)
+
+ def configure_fieldset(self, fs):
+ fs.configure(
+ include=[
+ fs.id,
+ fs.created,
+ fs.created_by,
+ fs.filename,
+ fs.device_type.with_renderer(forms.renderers.EnumFieldRenderer(enum.HANDHELD_DEVICE)),
+ fs.device_name,
+ fs.executed,
+ fs.executed_by,
+ ])
+ if self.creating:
+ del fs.id
+ elif self.viewing:
+ fs.append(fa.Field('inventory_batch', value=fs.model.inventory_batch, renderer=InventoryBatchFieldRenderer))
+
+ def configure_row_grid(self, g):
+ g.configure(
+ include=[
+ g.sequence,
+ g.upc.label("UPC"),
+ g.brand_name.label("Brand"),
+ g.description,
+ g.size,
+ g.cases,
+ g.units,
+ g.status_code.label("Status"),
+ ],
+ readonly=True)
+
+ def row_grid_row_attrs(self, row, i):
+ attrs = {}
+ if row.status_code == row.STATUS_PRODUCT_NOT_FOUND:
+ attrs['class_'] = 'warning'
+ return attrs
+
+ def get_exec_options_kwargs(self, **kwargs):
+ kwargs['ACTION_OPTIONS'] = list(ACTION_OPTIONS.iteritems())
+ return kwargs
+
+ def get_execute_success_url(self, batch, result, **kwargs):
+ if kwargs['action'] == 'make_inventory_batch':
+ return self.request.route_url('batch.inventory.view', uuid=result.uuid)
+ elif kwargs['action'] == 'make_label_batch':
+ return self.request.route_url('batch.rows', uuid=result.uuid)
+ return super(HandheldBatchView, self).get_execute_success_url(batch)
+
+ @classmethod
+ def defaults(cls, config):
+
+ # fix permission group title
+ config.add_tailbone_permission_group('batch.handheld', "Handheld Batches")
+
+ cls._batch_defaults(config)
+ cls._defaults(config)
+
+
+def includeme(config):
+ HandheldBatchView.defaults(config)
diff --git a/tailbone/views/inventory.py b/tailbone/views/inventory.py
new file mode 100644
index 00000000..49519a82
--- /dev/null
+++ b/tailbone/views/inventory.py
@@ -0,0 +1,110 @@
+# -*- coding: utf-8 -*-
+################################################################################
+#
+# Rattail -- Retail Software Framework
+# Copyright © 2010-2016 Lance Edgar
+#
+# This file is part of Rattail.
+#
+# Rattail is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
+# more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Rattail. If not, see .
+#
+################################################################################
+"""
+Views for inventory batches
+"""
+
+from __future__ import unicode_literals, absolute_import
+
+from rattail.db.batch.inventory.handler import InventoryBatchHandler
+
+import formalchemy as fa
+from webhelpers.html import tags
+
+from tailbone import forms
+from tailbone.views.batch import BatchMasterView
+
+from dtail.db import model
+
+
+class HandheldBatchFieldRenderer(fa.FieldRenderer):
+ """
+ Renderer for inventory batch's "handheld batch" field.
+ """
+
+ def render_readonly(self, **kwargs):
+ batch = self.raw_value
+ if batch:
+ return tags.link_to(
+ batch.id_str,
+ self.request.route_url('batch.handheld.view', uuid=batch.uuid))
+ return ''
+
+
+class InventoryBatchView(BatchMasterView):
+ """
+ Master view for inventory batches.
+ """
+ model_class = model.InventoryBatch
+ model_title_plural = "Inventory Batches"
+ batch_row_class = model.InventoryBatchRow
+ batch_handler_class = InventoryBatchHandler
+ route_prefix = 'batch.inventory'
+ url_prefix = '/batch/inventory'
+ creatable = False
+
+ def configure_fieldset(self, fs):
+ fs.configure(
+ include=[
+ fs.id,
+ fs.created,
+ fs.created_by,
+ fs.handheld_batch.with_renderer(HandheldBatchFieldRenderer),
+ fs.executed,
+ fs.executed_by,
+ ])
+ if not self.viewing:
+ del fs.handheld_batch
+
+ def configure_row_grid(self, g):
+ g.configure(
+ include=[
+ g.sequence,
+ g.upc.label("UPC"),
+ g.brand_name.label("Brand"),
+ g.description,
+ g.size,
+ g.cases,
+ g.units,
+ g.status_code.label("Status"),
+ ],
+ readonly=True)
+
+ def row_grid_row_attrs(self, row, i):
+ attrs = {}
+ if row.status_code == row.STATUS_PRODUCT_NOT_FOUND:
+ attrs['class_'] = 'warning'
+ return attrs
+
+ @classmethod
+ def defaults(cls, config):
+
+ # fix permission group title
+ config.add_tailbone_permission_group('batch.inventory', "Inventory Batches")
+
+ cls._batch_defaults(config)
+ cls._defaults(config)
+
+
+def includeme(config):
+ InventoryBatchView.defaults(config)