From 329e75ee822f7b2f796ede10a1dc14b7b28e1532 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sun, 31 Jan 2021 20:46:29 -0600 Subject: [PATCH] Add initial "scanning" feature for Ordering Batches --- tailbone/api/batch/receiving.py | 8 +- .../static/js/tailbone.buefy.numericinput.js | 23 +- tailbone/templates/batch/view.mako | 11 +- tailbone/templates/ordering/view.mako | 407 ++++++++++++++++++ tailbone/views/batch/core.py | 4 +- tailbone/views/purchasing/ordering.py | 132 +++++- 6 files changed, 553 insertions(+), 32 deletions(-) diff --git a/tailbone/api/batch/receiving.py b/tailbone/api/batch/receiving.py index 22632b80..37bb00b5 100644 --- a/tailbone/api/batch/receiving.py +++ b/tailbone/api/batch/receiving.py @@ -31,7 +31,6 @@ import logging import six import humanize -from rattail import pod from rattail.db import model from rattail.time import make_utc from rattail.util import pretty_quantity @@ -268,9 +267,12 @@ class ReceivingBatchRowViews(APIBatchRowView): return filters def normalize(self, row): - batch = row.batch data = super(ReceivingBatchRowViews, self).normalize(row) + batch = row.batch + app = self.get_rattail_app() + prodder = app.get_products_handler() + data['product_uuid'] = row.product_uuid data['item_id'] = row.item_id data['upc'] = six.text_type(row.upc) @@ -282,7 +284,7 @@ class ReceivingBatchRowViews(APIBatchRowView): # 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 + data['image_url'] = prodder.get_image_url(product=row.product, upc=row.upc) # unit_uom can vary by product data['unit_uom'] = 'LB' if row.product and row.product.weighed else 'EA' diff --git a/tailbone/static/js/tailbone.buefy.numericinput.js b/tailbone/static/js/tailbone.buefy.numericinput.js index 47a5e610..3fc0d74f 100644 --- a/tailbone/static/js/tailbone.buefy.numericinput.js +++ b/tailbone/static/js/tailbone.buefy.numericinput.js @@ -4,8 +4,14 @@ const NumericInput = { '', @@ -15,16 +21,25 @@ const NumericInput = { props: { name: String, value: String, + placeholder: String, + iconPack: String, + icon: String, + size: String, + disabled: Boolean, allowEnter: Boolean }, methods: { - focus(event) { + focus() { + this.$refs.input.focus() + }, + + notifyFocus(event) { this.$emit('focus', event) }, - blur(event) { + notifyBlur(event) { this.$emit('blur', event) }, diff --git a/tailbone/templates/batch/view.mako b/tailbone/templates/batch/view.mako index bfc22833..d1a640ed 100644 --- a/tailbone/templates/batch/view.mako +++ b/tailbone/templates/batch/view.mako @@ -96,8 +96,9 @@ <%def name="leading_buttons()"> % if master.has_worksheet and master.allow_worksheet(batch) and master.has_perm('worksheet'): % if use_buefy: - % else: @@ -110,10 +111,10 @@ % if master.batch_refreshable(batch) and master.has_perm('refresh'): % if use_buefy: ## TODO: this should surely use a POST request? - + icon-left="redo"> % else: diff --git a/tailbone/templates/ordering/view.mako b/tailbone/templates/ordering/view.mako index 9d2b7247..f0e6380a 100644 --- a/tailbone/templates/ordering/view.mako +++ b/tailbone/templates/ordering/view.mako @@ -13,4 +13,411 @@ % endif +<%def name="render_row_grid_tools()"> + ${parent.render_row_grid_tools()} + % if not batch.executed and not batch.complete and master.has_perm('edit_row'): + + + % endif + + +<%def name="render_this_page_template()"> + ${parent.render_this_page_template()} + % if not batch.executed and not batch.complete and master.has_perm('edit_row'): + + % endif + + +<%def name="modify_this_page_vars()"> + ${parent.modify_this_page_vars()} + % if not batch.executed and not batch.complete and master.has_perm('edit_row'): + + % endif + + +<%def name="make_this_page_component()"> + ${parent.make_this_page_component()} + % if not batch.executed and not batch.complete and master.has_perm('edit_row'): + + % endif + + + ${parent.body()} diff --git a/tailbone/views/batch/core.py b/tailbone/views/batch/core.py index d48a7913..3a07f0a8 100644 --- a/tailbone/views/batch/core.py +++ b/tailbone/views/batch/core.py @@ -412,10 +412,10 @@ class BatchMasterView(MasterView): return text if batch.complete: - label = "Mark as NOT Complete" + label = "Mark Incomplete" value = 'false' else: - label = "Mark as Complete" + label = "Mark Complete" value = 'true' kwargs = {} diff --git a/tailbone/views/purchasing/ordering.py b/tailbone/views/purchasing/ordering.py index 43955263..e184fd3f 100644 --- a/tailbone/views/purchasing/ordering.py +++ b/tailbone/views/purchasing/ordering.py @@ -169,6 +169,89 @@ class OrderingBatchView(PurchasingBatchView): if field not in editable_fields: f.set_readonly(field) + def scanning_entry(self): + """ + AJAX view to handle user entry/fetch input for "scanning" feature. + """ + data = self.request.json_body + app = self.get_rattail_app() + prodder = app.get_products_handler() + + batch = self.get_instance() + entry = data['entry'] + row = self.handler.quick_entry(self.Session(), batch, entry) + + uom = self.enum.UNIT_OF_MEASURE_EACH + if row.product and row.product.weighed: + uom = self.enum.UNIT_OF_MEASURE_POUND + + cases_ordered = None + if row.cases_ordered: + cases_ordered = float(row.cases_ordered) + + units_ordered = None + if row.units_ordered: + units_ordered = float(row.units_ordered) + + po_case_cost = None + if row.po_unit_cost is not None: + po_case_cost = row.po_unit_cost * (row.case_quantity or 1) + + product_url = None + if row.product_uuid: + product_url = self.request.route_url('products.view', uuid=row.product_uuid) + + product_price = None + if row.product and row.product.regular_price: + product_price = row.product.regular_price.price + + product_price_display = None + if product_price is not None: + product_price_display = app.render_currency(product_price) + + return { + 'ok': True, + 'entry': entry, + 'row': { + 'uuid': row.uuid, + 'item_id': row.item_id, + 'upc_display': row.upc.pretty() if row.upc else None, + 'brand_name': row.brand_name, + 'description': row.description, + 'size': row.size, + 'unit_of_measure_display': self.enum.UNIT_OF_MEASURE[uom], + 'case_quantity': float(row.case_quantity) if row.case_quantity is not None else None, + 'cases_ordered': cases_ordered, + 'units_ordered': units_ordered, + 'po_unit_cost': float(row.po_unit_cost) if row.po_unit_cost is not None else None, + 'po_unit_cost_display': app.render_currency(row.po_unit_cost), + 'po_case_cost': float(po_case_cost) if po_case_cost is not None else None, + 'po_case_cost_display': app.render_currency(po_case_cost), + 'image_url': prodder.get_image_url(upc=row.upc), + 'product_url': product_url, + 'product_price_display': product_price_display, + }, + } + + def scanning_update(self): + """ + AJAX view to handle row data updates for "scanning" feature. + """ + data = self.request.json_body + batch = self.get_instance() + assert batch.mode == self.enum.PURCHASE_BATCH_MODE_ORDERING + assert not (batch.executed or batch.complete) + + uuid = data.get('row_uuid') + row = self.Session.query(self.model_row_class).get(uuid) if uuid else None + if not row: + return {'error': "Row not found"} + if row.batch is not batch or row.removed: + return {'error': "Row is not active for batch"} + + self.handler.update_row_quantity(row, **data) + return {'ok': True} + def worksheet(self): """ View for editing batch row data as an order form worksheet. @@ -401,24 +484,6 @@ class OrderingBatchView(PurchasingBatchView): return self.request.route_url('purchases.view', uuid=result.uuid) return super(OrderingBatchView, self).get_execute_success_url(batch, result, **kwargs) - @classmethod - def _ordering_defaults(cls, config): - route_prefix = cls.get_route_prefix() - url_prefix = cls.get_url_prefix() - permission_prefix = cls.get_permission_prefix() - model_title = cls.get_model_title() - model_title_plural = cls.get_model_title_plural() - - # fix permission group label - config.add_tailbone_permission_group(permission_prefix, model_title_plural) - - # download as Excel - config.add_route('{}.download_excel'.format(route_prefix), '{}/{{uuid}}/excel'.format(url_prefix)) - config.add_view(cls, attr='download_excel', route_name='{}.download_excel'.format(route_prefix), - permission='{}.download_excel'.format(permission_prefix)) - config.add_tailbone_permission(permission_prefix, '{}.download_excel'.format(permission_prefix), - "Download {} as Excel".format(model_title)) - @classmethod def defaults(cls, config): cls._ordering_defaults(config) @@ -426,6 +491,37 @@ class OrderingBatchView(PurchasingBatchView): cls._batch_defaults(config) cls._defaults(config) + @classmethod + def _ordering_defaults(cls, config): + route_prefix = cls.get_route_prefix() + instance_url_prefix = cls.get_instance_url_prefix() + permission_prefix = cls.get_permission_prefix() + model_title = cls.get_model_title() + model_title_plural = cls.get_model_title_plural() + + # fix permission group label + config.add_tailbone_permission_group(permission_prefix, model_title_plural, + overwrite=False) + + # scanning entry + config.add_route('{}.scanning_entry'.format(route_prefix), '{}/scanning-entry'.format(instance_url_prefix)) + config.add_view(cls, attr='scanning_entry', route_name='{}.scanning_entry'.format(route_prefix), + permission='{}.edit_row'.format(permission_prefix), + renderer='json') + + # scanning update + config.add_route('{}.scanning_update'.format(route_prefix), '{}/scanning-update'.format(instance_url_prefix)) + config.add_view(cls, attr='scanning_update', route_name='{}.scanning_update'.format(route_prefix), + permission='{}.edit_row'.format(permission_prefix), + renderer='json') + + # download as Excel + config.add_route('{}.download_excel'.format(route_prefix), '{}/excel'.format(instance_url_prefix)) + config.add_view(cls, attr='download_excel', route_name='{}.download_excel'.format(route_prefix), + permission='{}.download_excel'.format(permission_prefix)) + config.add_tailbone_permission(permission_prefix, '{}.download_excel'.format(permission_prefix), + "Download {} as Excel".format(model_title)) + def includeme(config): OrderingBatchView.defaults(config)