diff --git a/docs/api/api/batch/core.rst b/docs/api/api/batch/core.rst new file mode 100644 index 00000000..48d34315 --- /dev/null +++ b/docs/api/api/batch/core.rst @@ -0,0 +1,15 @@ + +``tailbone.api.batch.core`` +=========================== + +.. automodule:: tailbone.api.batch.core + +.. autoclass:: APIBatchMixin + +.. autoclass:: APIBatchView + +.. autoclass:: APIBatchRowView + + .. autoattribute:: editable + + .. autoattribute:: supports_quick_entry diff --git a/docs/api/api/batch/ordering.rst b/docs/api/api/batch/ordering.rst new file mode 100644 index 00000000..4b07e1f2 --- /dev/null +++ b/docs/api/api/batch/ordering.rst @@ -0,0 +1,41 @@ + +``tailbone.api.batch.ordering`` +=============================== + +.. automodule:: tailbone.api.batch.ordering + +.. autoclass:: OrderingBatchViews + + .. autoattribute:: collection_url_prefix + + .. autoattribute:: object_url_prefix + + .. autoattribute:: model_class + + .. autoattribute:: route_prefix + + .. autoattribute:: permission_prefix + + .. autoattribute:: default_handler_spec + + .. automethod:: base_query + + .. automethod:: create_object + +.. autoclass:: OrderingBatchRowViews + + .. autoattribute:: collection_url_prefix + + .. autoattribute:: object_url_prefix + + .. autoattribute:: model_class + + .. autoattribute:: route_prefix + + .. autoattribute:: permission_prefix + + .. autoattribute:: default_handler_spec + + .. autoattribute:: supports_quick_entry + + .. automethod:: update_object diff --git a/docs/conf.py b/docs/conf.py index 87de553a..f96b4fec 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -41,6 +41,7 @@ extensions = [ ] intersphinx_mapping = { + 'rattail': ('https://rattailproject.org/docs/rattail/', None), 'webhelpers2': ('https://webhelpers2.readthedocs.io/en/latest/', None), } diff --git a/docs/index.rst b/docs/index.rst index 3157ae40..bc7d3005 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -42,6 +42,8 @@ Package API: .. toctree:: :maxdepth: 1 + api/api/batch/core + api/api/batch/ordering api/forms api/grids api/progress diff --git a/tailbone/api/batch/core.py b/tailbone/api/batch/core.py index 9f056d9f..4a24603e 100644 --- a/tailbone/api/batch/core.py +++ b/tailbone/api/batch/core.py @@ -219,6 +219,7 @@ class APIBatchRowView(APIBatchMixin, APIMasterView): """ Base class for all API views which are meant to handle "batch rows" data. """ + editable = False supports_quick_entry = False def __init__(self, request, **kwargs): @@ -280,6 +281,8 @@ class APIBatchRowView(APIBatchMixin, APIMasterView): resource.add_view(cls.collection_get, permission='{}.view'.format(permission_prefix)) resource.add_view(cls.get, permission='{}.view'.format(permission_prefix)) + if cls.editable: + resource.add_view(cls.post, permission='{}.edit'.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) diff --git a/tailbone/api/batch/ordering.py b/tailbone/api/batch/ordering.py index a1bdf4e4..4907ffe7 100644 --- a/tailbone/api/batch/ordering.py +++ b/tailbone/api/batch/ordering.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2019 Lance Edgar +# Copyright © 2010-2020 Lance Edgar # # This file is part of Rattail. # @@ -22,6 +22,9 @@ ################################################################################ """ Tailbone Web API - Ordering Batches + +These views expose the basic CRUD interface to "ordering" batches, for the web +API. """ from __future__ import unicode_literals, absolute_import @@ -29,73 +32,137 @@ from __future__ import unicode_literals, absolute_import import six from rattail.db import model -from rattail.time import localtime +from rattail.util import pretty_quantity -from cornice.resource import resource, view - -from tailbone.api import APIMasterView +from tailbone.api.batch import APIBatchView, APIBatchRowView -@resource(collection_path='/ordering-batches', path='/ordering-batch/{uuid}') -class OrderingBatchView(APIMasterView): +class OrderingBatchViews(APIBatchView): model_class = model.PurchaseBatch + default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler' + route_prefix = 'orderingbatchviews' + permission_prefix = 'ordering' + collection_url_prefix = '/ordering-batches' + object_url_prefix = '/ordering-batch' def base_query(self): - return self.Session.query(model.PurchaseBatch)\ - .filter(model.PurchaseBatch.mode == self.enum.PURCHASE_BATCH_MODE_ORDERING) + """ + Modifies the default logic as follows: - def pretty_datetime(self, dt): - if not dt: - return "" - return dt.strftime('%Y-%m-%d @ %I:%M %p') + Adds a condition to the query, to ensure only purchase batches with + "ordering" mode are returned. + """ + query = super(OrderingBatchViews, self).base_query() + query = query.filter(model.PurchaseBatch.mode == self.enum.PURCHASE_BATCH_MODE_ORDERING) + return query def normalize(self, batch): + data = super(OrderingBatchViews, self).normalize(batch) - created = batch.created - created = localtime(self.rattail_config, created, from_utc=True) - created = self.pretty_datetime(created) + data['vendor_uuid'] = batch.vendor.uuid + data['vendor_display'] = six.text_type(batch.vendor) - executed = batch.executed - if executed: - executed = localtime(self.rattail_config, executed, from_utc=True) - executed = self.pretty_datetime(executed) + data['department_uuid'] = batch.department_uuid + data['department_display'] = six.text_type(batch.department) if batch.department else None - return { - 'uuid': batch.uuid, - '_str': six.text_type(batch), - 'id': batch.id, - 'id_str': batch.id_str, - 'description': batch.description, - 'vendor_uuid': batch.vendor.uuid, - 'vendor_name': batch.vendor.name, - 'po_total_calculated': batch.po_total_calculated, - 'po_total_calculated_display': "${:0.2f}".format(batch.po_total_calculated) if batch.po_total_calculated is not None else None, - 'date_ordered': six.text_type(batch.date_ordered or ''), - 'created': created, - 'created_by_uuid': batch.created_by.uuid, - 'created_by_display': six.text_type(batch.created_by), - 'executed': executed, - 'executed_by_uuid': batch.executed_by_uuid, - 'executed_by_display': six.text_type(batch.executed_by or ''), - } + data['po_total_calculated_display'] = "${:0.2f}".format(batch.po_total_calculated) if batch.po_total_calculated is not None else None + + return data + + def create_object(self, data): + """ + Modifies the default logic as follows: + + Sets the mode to "ordering" for the new batch. + """ + data = dict(data) + data['mode'] = self.enum.PURCHASE_BATCH_MODE_ORDERING + batch = super(OrderingBatchViews, self).create_object(data) + return batch - @view(permission='ordering.list') def collection_get(self): return self._collection_get() - # @view(permission='ordering.create') - # def collection_post(self): - # return self._collection_post() + def collection_post(self): + return self._collection_post() - @view(permission='ordering.view') def get(self): return self._get() - # @view(permission='ordering.edit') - # def post(self): - # return self._post() + +class OrderingBatchRowViews(APIBatchRowView): + + model_class = model.PurchaseBatchRow + default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler' + route_prefix = 'ordering.rows' + permission_prefix = 'ordering' + collection_url_prefix = '/ordering-batch-rows' + object_url_prefix = '/ordering-batch-row' + supports_quick_entry = True + + def normalize(self, row): + batch = row.batch + data = super(OrderingBatchRowViews, 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['brand_name'] = row.brand_name + data['description'] = row.description + # data['size'] = row.size + # data['full_description'] = row.product.full_description if row.product else row.description + + # # only provide image url if so configured + # if self.rattail_config.getbool('rattail.batch', 'purchase.mobile_images', default=True): + # data['image_url'] = pod.get_image_url(self.rattail_config, row.upc) if row.upc else None + + # # unit_uom can vary by product + # data['unit_uom'] = 'LB' if row.product and row.product.weighed else 'EA' + + # data['case_quantity'] = row.case_quantity + # data['order_quantities_known'] = batch.order_quantities_known + + data['cases_ordered'] = row.cases_ordered + data['units_ordered'] = row.units_ordered + + data['units_ordered_display'] = pretty_quantity(row.units_ordered or 0, empty_zero=False) + # 'po_unit_cost': row.po_unit_cost, + data['po_unit_cost_display'] = "${:0.2f}".format(row.po_unit_cost) if row.po_unit_cost is not None else None + # 'po_total_calculated': row.po_total_calculated, + data['po_total_calculated_display'] = "${:0.2f}".format(row.po_total_calculated) if row.po_total_calculated is not None else None + # 'status_code': row.status_code, + # 'status_display': row.STATUS.get(row.status_code, six.text_type(row.status_code)), + + return data + + def collection_get(self): + return self._collection_get() + + def get(self): + return self._get() + + def post(self): + return self._post() + + def update_object(self, row, data): + """ + Overrides the default logic as follows: + + So far, we only allow updating the ``cases_ordered`` and/or + ``units_ordered`` quantities; therefore ``data`` should have one or + both of those keys. + + This data is then passed to the + :meth:`~rattail:rattail.batch.purchase.PurchaseBatchHandler.update_row_quantity()` + method of the batch handler. + + Note that the "normal" logic for this method is not invoked at all. + """ + self.handler.update_row_quantity(row, **data) + return row def includeme(config): - config.scan(__name__) + OrderingBatchViews.defaults(config) + OrderingBatchRowViews.defaults(config)