Add some API views for receiving, and vendor autocomplete

lots more to do yet, for those...
This commit is contained in:
Lance Edgar 2019-11-12 11:55:28 -06:00
parent afdd294c60
commit 3514c4050e
7 changed files with 409 additions and 130 deletions

View file

@ -26,4 +26,4 @@ Tailbone Web API - Batches
from __future__ import unicode_literals, absolute_import from __future__ import unicode_literals, absolute_import
from .core import BatchAPIMasterView from .core import APIBatchView, APIBatchRowView, BatchAPIMasterView

View file

@ -28,26 +28,22 @@ from __future__ import unicode_literals, absolute_import
import six import six
from rattail.time import localtime
from rattail.util import load_object from rattail.util import load_object
from cornice import resource
from tailbone.api import APIMasterView from tailbone.api import APIMasterView
class BatchAPIMasterView(APIMasterView): class APIBatchMixin(object):
""" """
Base class for all API views which are meant to handle "batch" *and/or* Base class for all API views which are meant to handle "batch" *and/or*
"batch row" data. "batch row" data.
""" """
supports_quick_entry = False
supports_toggle_complete = False
def __init__(self, request, **kwargs): def get_batch_class(self):
super(BatchAPIMasterView, self).__init__(request, **kwargs) model_class = self.get_model_class()
self.handler = self.get_handler()
@classmethod
def get_batch_class(cls):
model_class = cls.get_model_class()
if hasattr(model_class, '__batch_class__'): if hasattr(model_class, '__batch_class__'):
return model_class.__batch_class__ return model_class.__batch_class__
return model_class return model_class
@ -74,27 +70,75 @@ class BatchAPIMasterView(APIMasterView):
default=self.default_handler_spec) default=self.default_handler_spec)
return load_object(spec)(self.rattail_config) return load_object(spec)(self.rattail_config)
def quick_entry(self):
class APIBatchView(APIBatchMixin, APIMasterView):
""" """
View for handling "quick entry" user input, for a batch. Base class for all API views which are meant to handle "batch" *and/or*
"batch row" data.
""" """
data = self.request.json_body supports_toggle_complete = False
uuid = data['batch_uuid'] def __init__(self, request, **kwargs):
batch = self.Session.query(self.get_batch_class()).get(uuid) super(APIBatchView, self).__init__(request, **kwargs)
if not batch: self.handler = self.get_handler()
raise self.notfound()
entry = data['quick_entry'] def normalize(self, batch):
try: created = batch.created
row = self.handler.quick_entry(self.Session(), batch, entry) created = localtime(self.rattail_config, created, from_utc=True)
except Exception as error: created = self.pretty_datetime(created)
return {'error': six.text_type(error)}
result = self._get(obj=row) executed = batch.executed
result['ok'] = True if executed:
return result executed = localtime(self.rattail_config, executed, from_utc=True)
executed = self.pretty_datetime(executed)
return {
'uuid': batch.uuid,
'_str': six.text_type(batch),
'id': batch.id,
'id_str': batch.id_str,
'description': batch.description,
'notes': batch.notes,
'rowcount': batch.rowcount,
'created': created,
'created_by_uuid': batch.created_by.uuid,
'created_by_display': six.text_type(batch.created_by),
'complete': batch.complete,
'executed': executed,
'executed_by_uuid': batch.executed_by_uuid,
'executed_by_display': six.text_type(batch.executed_by or ''),
}
def create_object(self, data):
"""
Create a new object instance and populate it with the given data.
Here we'll invoke the handler for actual batch creation, instead of
typical logic used for simple records.
"""
kwargs = dict(data)
kwargs['user'] = self.request.user
batch = self.handler.make_batch(self.Session(), **kwargs)
return batch
def update_object(self, batch, data):
"""
Logic for updating a main object record.
Here we want to make sure we set "created by" to the current user, when
creating a new batch.
"""
# we're only concerned with *new* batches here
if not batch.uuid:
# assign creator; initialize row count
batch.created_by_uuid = self.request.user.uuid
if batch.rowcount is None:
batch.rowcount = 0
# then go ahead with usual logic
return super(APIBatchView, self).update_object(batch, data)
def mark_complete(self): def mark_complete(self):
""" """
@ -130,21 +174,24 @@ class BatchAPIMasterView(APIMasterView):
batch.complete = False batch.complete = False
return self._get(obj=batch) return self._get(obj=batch)
@classmethod
def defaults(cls, config):
cls._batch_defaults(config)
@classmethod @classmethod
def _batch_defaults(cls, config): def _batch_defaults(cls, config):
route_prefix = cls.get_route_prefix() route_prefix = cls.get_route_prefix()
permission_prefix = cls.get_permission_prefix()
collection_url_prefix = cls.get_collection_url_prefix() collection_url_prefix = cls.get_collection_url_prefix()
object_url_prefix = cls.get_object_url_prefix() object_url_prefix = cls.get_object_url_prefix()
permission_prefix = cls.get_permission_prefix()
if cls.supports_quick_entry: # primary / typical API
resource.add_view(cls.collection_get, permission='{}.list'.format(permission_prefix))
# quick entry resource.add_view(cls.collection_post, permission='{}.create'.format(permission_prefix))
config.add_route('{}.quick_entry'.format(route_prefix), '{}/quick-entry'.format(collection_url_prefix), resource.add_view(cls.get, permission='{}.view'.format(permission_prefix))
request_method=('OPTIONS', 'POST')) batch_resource = resource.add_resource(cls, collection_path=collection_url_prefix,
config.add_view(cls, attr='quick_entry', route_name='{}.quick_entry'.format(route_prefix), path='{}/{{uuid}}'.format(object_url_prefix))
permission='{}.edit'.format(permission_prefix), config.add_cornice_resource(batch_resource)
renderer='json')
if cls.supports_toggle_complete: if cls.supports_toggle_complete:
@ -160,3 +207,84 @@ class BatchAPIMasterView(APIMasterView):
permission='{}.edit'.format(permission_prefix), permission='{}.edit'.format(permission_prefix),
renderer='json') renderer='json')
# TODO: deprecate / remove this
BatchAPIMasterView = APIBatchView
class APIBatchRowView(APIBatchMixin, APIMasterView):
"""
Base class for all API views which are meant to handle "batch rows" data.
"""
supports_quick_entry = False
def __init__(self, request, **kwargs):
super(APIBatchRowView, self).__init__(request, **kwargs)
self.handler = self.get_handler()
def normalize(self, row):
batch = row.batch
return {
'uuid': row.uuid,
'_str': six.text_type(row),
'_parent_str': six.text_type(batch),
'_parent_uuid': batch.uuid,
'batch_uuid': batch.uuid,
'batch_id': batch.id,
'batch_id_str': batch.id_str,
'batch_description': batch.description,
'sequence': row.sequence,
'status_code': row.status_code,
'status_display': row.STATUS.get(row.status_code, six.text_type(row.status_code)),
}
def quick_entry(self):
"""
View for handling "quick entry" user input, for a batch.
"""
data = self.request.json_body
uuid = data['batch_uuid']
batch = self.Session.query(self.get_batch_class()).get(uuid)
if not batch:
raise self.notfound()
entry = data['quick_entry']
try:
row = self.handler.quick_entry(self.Session(), batch, entry)
except Exception as error:
msg = six.text_type(error)
if not msg and isinstance(error, NotImplementedError):
msg = "Feature is not implemented"
return {'error': msg}
result = self._get(obj=row)
result['ok'] = True
return result
@classmethod
def defaults(cls, config):
cls._batch_row_defaults(config)
@classmethod
def _batch_row_defaults(cls, config):
route_prefix = cls.get_route_prefix()
permission_prefix = cls.get_permission_prefix()
collection_url_prefix = cls.get_collection_url_prefix()
object_url_prefix = cls.get_object_url_prefix()
resource.add_view(cls.collection_get, permission='{}.view'.format(permission_prefix))
resource.add_view(cls.get, permission='{}.view'.format(permission_prefix))
rows_resource = resource.add_resource(cls, collection_path=collection_url_prefix,
path='{}/{{uuid}}'.format(object_url_prefix))
config.add_cornice_resource(rows_resource)
if cls.supports_quick_entry:
# quick entry
config.add_route('{}.quick_entry'.format(route_prefix), '{}/quick-entry'.format(collection_url_prefix),
request_method=('OPTIONS', 'POST'))
config.add_view(cls, attr='quick_entry', route_name='{}.quick_entry'.format(route_prefix),
permission='{}.edit'.format(permission_prefix),
renderer='json')

View file

@ -29,119 +29,50 @@ from __future__ import unicode_literals, absolute_import
import six import six
from rattail.db import model from rattail.db import model
from rattail.time import localtime
from cornice import resource from tailbone.api.batch import APIBatchView, APIBatchRowView
from tailbone.api.batch import BatchAPIMasterView
class LabelBatchViews(BatchAPIMasterView): class LabelBatchViews(APIBatchView):
model_class = model.LabelBatch model_class = model.LabelBatch
default_handler_spec = 'rattail.batch.labels:LabelBatchHandler' default_handler_spec = 'rattail.batch.labels:LabelBatchHandler'
route_prefix = 'labelbatchviews'
permission_prefix = 'labels.batch' permission_prefix = 'labels.batch'
collection_url_prefix = '/label-batches'
object_url_prefix = '/label-batch' object_url_prefix = '/label-batch'
supports_toggle_complete = True supports_toggle_complete = True
def pretty_datetime(self, dt):
if not dt:
return ""
return dt.strftime('%Y-%m-%d @ %I:%M %p')
def normalize(self, batch):
created = batch.created
created = localtime(self.rattail_config, created, from_utc=True)
created = self.pretty_datetime(created)
executed = batch.executed
if executed:
executed = localtime(self.rattail_config, executed, from_utc=True)
executed = self.pretty_datetime(executed)
return {
'uuid': batch.uuid,
'_str': six.text_type(batch),
'id': batch.id,
'id_str': batch.id_str,
'description': batch.description,
'notes': batch.notes,
'rowcount': batch.rowcount,
'created': created,
'created_by_uuid': batch.created_by.uuid,
'created_by_display': six.text_type(batch.created_by),
'complete': batch.complete,
'executed': executed,
'executed_by_uuid': batch.executed_by_uuid,
'executed_by_display': six.text_type(batch.executed_by or ''),
}
def collection_get(self): def collection_get(self):
return self._collection_get() return self._collection_get()
def collection_post(self): def collection_post(self):
return self._collection_post() return self._collection_post()
def update_object(self, batch, data):
# assign some default values for new batch
if not batch.uuid:
batch.created_by_uuid = self.request.user.uuid
if batch.rowcount is None:
batch.rowcount = 0
return super(LabelBatchViews, self).update_object(batch, data)
def get(self): def get(self):
return self._get() return self._get()
# @view(permission='labels.batch.edit')
# def post(self):
# return self._post()
@classmethod class LabelBatchRowViews(APIBatchRowView):
def defaults(cls, config):
# label batches
resource.add_view(cls.collection_get, permission='labels.batch.list')
resource.add_view(cls.collection_post, permission='labels.batch.create')
resource.add_view(cls.get, permission='labels.batch.view')
batch_resource = resource.add_resource(cls, collection_path='/label-batches', path='/label-batch/{uuid}')
config.add_cornice_resource(batch_resource)
cls._batch_defaults(config)
class LabelBatchRowViews(BatchAPIMasterView):
model_class = model.LabelBatchRow model_class = model.LabelBatchRow
default_handler_spec = 'rattail.batch.labels:LabelBatchHandler' default_handler_spec = 'rattail.batch.labels:LabelBatchHandler'
route_prefix = 'api.label_batch_rows' route_prefix = 'api.label_batch_rows'
permission_prefix = 'labels.batch' permission_prefix = 'labels.batch'
collection_url_prefix = '/label-batch-rows' collection_url_prefix = '/label-batch-rows'
object_url_prefix = '/label-batch-row'
supports_quick_entry = True supports_quick_entry = True
def normalize(self, row): def normalize(self, row):
batch = row.batch batch = row.batch
return { data = super(LabelBatchRowViews, self).normalize(row)
'uuid': row.uuid,
'_str': six.text_type(row), data['item_id'] = row.item_id
'_parent_str': six.text_type(batch), data['upc'] = six.text_type(row.upc)
'_parent_uuid': batch.uuid, data['upc_pretty'] = row.upc.pretty() if row.upc else None
'batch_uuid': batch.uuid, data['description'] = row.description
'batch_id': batch.id, data['full_description'] = row.product.full_description if row.product else row.description
'batch_id_str': batch.id_str, return data
'batch_description': batch.description,
'sequence': row.sequence,
'item_id': row.item_id,
'upc': six.text_type(row.upc),
'upc_pretty': row.upc.pretty() if row.upc else None,
'description': row.description,
'full_description': row.product.full_description if row.product else row.description,
'status_code': row.status_code,
'status_display': row.STATUS.get(row.status_code, six.text_type(row.status_code)),
}
def collection_get(self): def collection_get(self):
return self._collection_get() return self._collection_get()
@ -149,18 +80,6 @@ class LabelBatchRowViews(BatchAPIMasterView):
def get(self): def get(self):
return self._get() return self._get()
@classmethod
def defaults(cls, config):
permission_prefix = cls.get_permission_prefix()
# label batch rows
resource.add_view(cls.collection_get, permission='{}.view'.format(permission_prefix))
resource.add_view(cls.get, permission='{}.view'.format(permission_prefix))
rows_resource = resource.add_resource(cls, collection_path='/label-batch-rows', path='/label-batch-row/{uuid}')
config.add_cornice_resource(rows_resource)
cls._batch_defaults(config)
def includeme(config): def includeme(config):
LabelBatchViews.defaults(config) LabelBatchViews.defaults(config)

View file

@ -0,0 +1,99 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2019 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 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 General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Tailbone Web API - Receiving Batches
"""
from __future__ import unicode_literals, absolute_import
import six
from rattail.db import model
from tailbone.api.batch import APIBatchView, APIBatchRowView
class ReceivingBatchViews(APIBatchView):
model_class = model.PurchaseBatch
default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler'
route_prefix = 'receivingbatchviews'
permission_prefix = 'receiving'
collection_url_prefix = '/receiving-batches'
object_url_prefix = '/receiving-batch'
def normalize(self, batch):
data = super(ReceivingBatchViews, self).normalize(batch)
data['vendor_uuid'] = batch.vendor.uuid
data['vendor_display'] = six.text_type(batch.vendor)
return data
def create_object(self, data):
data = dict(data)
data['mode'] = self.enum.PURCHASE_BATCH_MODE_RECEIVING
batch = super(ReceivingBatchViews, self).create_object(data)
return batch
def collection_get(self):
return self._collection_get()
def collection_post(self):
return self._collection_post()
def get(self):
return self._get()
class ReceivingBatchRowViews(APIBatchRowView):
model_class = model.PurchaseBatchRow
default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler'
route_prefix = 'receiving.rows'
permission_prefix = 'receiving'
collection_url_prefix = '/receiving-batch-rows'
object_url_prefix = '/receiving-batch-row'
supports_quick_entry = True
def normalize(self, row):
batch = row.batch
data = super(ReceivingBatchRowViews, self).normalize(row)
data['item_id'] = row.item_id
data['upc'] = six.text_type(row.upc)
data['upc_pretty'] = row.upc.pretty() if row.upc else None
data['description'] = row.description
data['full_description'] = row.product.full_description if row.product else row.description
return data
def collection_get(self):
return self._collection_get()
def get(self):
return self._get()
def includeme(config):
ReceivingBatchViews.defaults(config)
ReceivingBatchRowViews.defaults(config)

View file

@ -63,3 +63,8 @@ class APIView(View):
""" """
Base class for all API views. Base class for all API views.
""" """
def pretty_datetime(self, dt):
if not dt:
return ""
return dt.strftime('%Y-%m-%d @ %I:%M %p')

View file

@ -27,6 +27,7 @@ Tailbone Web API - Master View
from __future__ import unicode_literals, absolute_import from __future__ import unicode_literals, absolute_import
import json import json
import six
from rattail.config import parse_bool from rattail.config import parse_bool
@ -364,3 +365,55 @@ class APIMasterView(APIView):
# that's all we can do here, subclass must override if more needed # that's all we can do here, subclass must override if more needed
return obj return obj
##############################
# autocomplete
##############################
def autocomplete(self):
term = self.request.params.get('term', '').strip()
term = self.prepare_autocomplete_term(term)
if not term:
return []
results = self.get_autocomplete_data(term)
return [{'label': self.autocomplete_display(x),
'value': self.autocomplete_value(x)}
for x in results]
@property
def autocomplete_fieldname(self):
raise NotImplementedError("You must define `autocomplete_fieldname` "
"attribute for API view class: {}".format(
self.__class__))
def autocomplete_display(self, obj):
return getattr(obj, self.autocomplete_fieldname)
def autocomplete_value(self, obj):
return obj.uuid
def get_autocomplete_data(self, term):
query = self.make_autocomplete_query(term)
return query.all()
def make_autocomplete_query(self, term):
model_class = self.get_model_class()
query = self.Session.query(model_class)
query = self.filter_autocomplete_query(query)
field = getattr(model_class, self.autocomplete_fieldname)
query = query.filter(field.ilike('%%%s%%' % term))\
.order_by(field)
return query
def filter_autocomplete_query(self, query):
return query
def prepare_autocomplete_term(self, term):
"""
If necessary, massage the incoming search term for use with the
autocomplete query.
"""
return term

75
tailbone/api/vendors.py Normal file
View file

@ -0,0 +1,75 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2019 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 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 General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Tailbone Web API - Vendor Views
"""
from __future__ import unicode_literals, absolute_import
import six
from rattail.db import model
from cornice import Service
from cornice.resource import resource, view
from tailbone.api import APIMasterView
@resource(collection_path='/vendors', path='/vendor/{uuid}')
class VendorView(APIMasterView):
model_class = model.Vendor
autocomplete_fieldname = 'name'
def normalize(self, vendor):
return {
'uuid': vendor.uuid,
'_str': six.text_type(vendor),
'id': vendor.id,
'name': vendor.name,
}
@view(permission='vendors.list')
def collection_get(self):
return self._collection_get()
@view(permission='vendors.create')
def collection_post(self):
return self._collection_post()
@view(permission='vendors.view')
def get(self):
return self._get()
@view(permission='vendors.edit')
def post(self):
return self._post()
def includeme(config):
config.scan(__name__)
autocomplete = Service(name='vendors.autocomplete', path='/vendors/autocomplete')
autocomplete.add_view('GET', 'autocomplete', klass=VendorView)
config.add_cornice_service(autocomplete)