From ed252c6465b6071227a23abb043ddf7ca7fb6785 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 13 Dec 2016 22:28:50 -0600 Subject: [PATCH] Overhaul the Receiving Form to account for "product not found" etc. Also shows ordered/received/etc. quantities --- .../purchases/batches/receive_form.mako | 145 +++++++++++++++-- tailbone/views/purchases/batch.py | 153 +++++++++++++----- 2 files changed, 245 insertions(+), 53 deletions(-) diff --git a/tailbone/templates/purchases/batches/receive_form.mako b/tailbone/templates/purchases/batches/receive_form.mako index 68fe30b6..7bb6a60f 100644 --- a/tailbone/templates/purchases/batches/receive_form.mako +++ b/tailbone/templates/purchases/batches/receive_form.mako @@ -23,16 +23,37 @@ function invalid_product(msg) { $('#received-product-info p').text(msg); $('#received-product-info img').hide(); - $('#received-product-info .rogue-item-warning').hide(); - $('#product-textbox').focus().select(); + $('#upc').focus().select(); $('.field-wrapper.cases input').prop('disabled', true); $('.field-wrapper.units input').prop('disabled', true); $('.buttons button').button('disable'); } + function pretty_quantity(cases, units) { + if (cases && units) { + return cases + " cases, " + units + " units"; + } else if (cases) { + return cases + " cases"; + } else if (units) { + return units + " units"; + } + return ''; + } + + function show_quantity(name, cases, units) { + var quantity = pretty_quantity(cases, units); + var field = $('.field-wrapper.quantity_' + name); + field.find('.field').text(quantity); + if (quantity || name == 'ordered') { + field.show(); + } else { + field.hide(); + } + } + $(function() { - $('#product-textbox').keydown(function(event) { + $('#upc').keydown(function(event) { if (key_allowed(event)) { return true; @@ -41,35 +62,75 @@ $('#product').val(''); $('#received-product-info p').html("please ENTER a scancode"); $('#received-product-info img').hide(); - $('#received-product-info .rogue-item-warning').hide(); + $('#received-product-info .warning').hide(); + $('.product-fields').hide(); + $('.receiving-fields').hide(); $('.field-wrapper.cases input').prop('disabled', true); $('.field-wrapper.units input').prop('disabled', true); $('.buttons button').button('disable'); return true; } + + // when user presses ENTER, do product lookup if (event.which == 13) { - var input = $(this); - var data = {upc: input.val()}; + var upc = $(this).val(); + var data = {'upc': upc}; $.get('${url('purchases.batch.receiving_lookup', uuid=batch.uuid)}', data, function(data) { + if (data.error) { alert(data.error); if (data.redirect) { $('#receiving-form').mask("Redirecting..."); location.href = data.redirect; } + } else if (data.product) { - input.val(data.product.upc_pretty); + $('#upc').val(data.product.upc_pretty); $('#product').val(data.product.uuid); + $('#brand_name').val(data.product.brand_name); + $('#description').val(data.product.description); + $('#size').val(data.product.size); + $('#case_quantity').val(data.product.case_quantity); + $('#received-product-info p').text(data.product.full_description); $('#received-product-info img').attr('src', data.product.image_url).show(); - $('#received-product-info .rogue-item-warning').hide(); - if (! data.product.found_in_batch) { - $('#received-product-info .rogue-item-warning').show(); + if (! data.product.uuid) { + // $('#received-product-info .warning.notfound').show(); + $('.product-fields').show(); + } + if (data.product.found_in_batch) { + show_quantity('ordered', data.product.cases_ordered, data.product.units_ordered); + show_quantity('received', data.product.cases_received, data.product.units_received); + show_quantity('damaged', data.product.cases_damaged, data.product.units_damaged); + show_quantity('expired', data.product.cases_expired, data.product.units_expired); + show_quantity('mispick', data.product.cases_mispick, data.product.units_mispick); + $('.receiving-fields').show(); + } else { + $('#received-product-info .warning.notordered').show(); } $('.field-wrapper.cases input').prop('disabled', false); $('.field-wrapper.units input').prop('disabled', false); $('.buttons button').button('enable'); $('#cases').focus().select(); + + } else if (data.upc) { + $('#upc').val(data.upc_pretty); + $('#received-product-info p').text("product not found in our system"); + $('#received-product-info img').attr('src', data.image_url).show(); + + $('#product').val(''); + $('#brand_name').val(''); + $('#description').val(''); + $('#size').val(''); + $('#case_quantity').val(''); + + $('#received-product-info .warning.notfound').show(); + $('.product-fields').show(); + $('#brand_name').focus(); + $('.field-wrapper.cases input').prop('disabled', false); + $('.field-wrapper.units input').prop('disabled', false); + $('.buttons button').button('enable'); + } else { invalid_product('product not found'); } @@ -212,7 +273,7 @@ $(this).mask("Working..."); }); - $('#product-textbox').focus(); + $('#upc').focus(); $('.field-wrapper.cases input').prop('disabled', true); $('.field-wrapper.units input').prop('disabled', true); $('.buttons button').button('disable'); @@ -235,7 +296,7 @@ margin: 0.5em 0; } - .product-info .rogue-item-warning { + #received-product-info .warning { background: #f66; display: none; } @@ -270,18 +331,72 @@ ${h.hidden('ordered_product')}
- +
${h.hidden('product')} -
${h.text('product-textbox', autocomplete='off')}
+
${h.text('upc', autocomplete='off')}

please ENTER a scancode

-
warning: product not found on current purchase
+
please confirm UPC and provide more details
+
warning: product not found on current purchase
+ + + +
${h.text('cases', autocomplete='off')}
diff --git a/tailbone/views/purchases/batch.py b/tailbone/views/purchases/batch.py index 1be60814..1b910af3 100644 --- a/tailbone/views/purchases/batch.py +++ b/tailbone/views/purchases/batch.py @@ -33,10 +33,11 @@ from sqlalchemy import orm from rattail import pod from rattail.db import model, api +from rattail.db.util import make_full_description from rattail.gpc import GPC from rattail.time import localtime from rattail.core import Object -from rattail.util import OrderedDict +from rattail.util import OrderedDict, pretty_quantity import formalchemy as fa import formencode as fe @@ -55,8 +56,13 @@ class ReceivingForm(forms.Schema): filter_extra_fields = True mode = fe.validators.OneOf(['received', 'damaged', 'expired', 'mispick']) product = forms.validators.ValidProduct() - cases = fe.validators.Int() - units = fe.validators.Int() + upc = forms.validators.ValidGPC() + brand_name = fe.validators.String() + description = fe.validators.String() + size = fe.validators.String() + case_quantity = fe.validators.Number() + cases = fe.validators.Number() + units = fe.validators.Number() expiration_date = fe.validators.DateValidator() ordered_product = forms.validators.ValidProduct() @@ -361,8 +367,10 @@ class PurchaseBatchView(BatchMasterView): def row_grid_row_attrs(self, row, i): attrs = {} - if row.status_code in (row.STATUS_INCOMPLETE, - row.STATUS_ORDERED_RECEIVED_DIFFER): + if row.status_code == row.STATUS_PRODUCT_NOT_FOUND: + attrs['class_'] = 'warning' + elif row.status_code in (row.STATUS_INCOMPLETE, + row.STATUS_ORDERED_RECEIVED_DIFFER): attrs['class_'] = 'notice' return attrs @@ -409,6 +417,9 @@ class PurchaseBatchView(BatchMasterView): fs.item_lookup, fs.upc, fs.product, + fs.brand_name, + fs.description, + fs.size, fs.case_quantity, fs.cases_ordered, fs.units_ordered, @@ -450,6 +461,12 @@ class PurchaseBatchView(BatchMasterView): elif self.viewing: del fs.item_lookup + if fs.model.product: + del (fs.brand_name, + fs.description, + fs.size) + else: + del fs.product def before_create_row(self, form): row = form.fieldset.model @@ -615,8 +632,8 @@ class PurchaseBatchView(BatchMasterView): if row.po_total and not row.removed: batch.po_total -= row.po_total if cases_ordered or units_ordered: - row.cases_ordered = cases_ordered - row.units_ordered = units_ordered + row.cases_ordered = cases_ordered or None + row.units_ordered = units_ordered or None row.removed = False self.handler.refresh_row(row) else: @@ -627,13 +644,13 @@ class PurchaseBatchView(BatchMasterView): row.sequence = max([0] + [r.sequence for r in batch.data_rows]) + 1 row.product = product batch.data_rows.append(row) - row.cases_ordered = cases_ordered - row.units_ordered = units_ordered + row.cases_ordered = cases_ordered or None + row.units_ordered = units_ordered or None self.handler.refresh_row(row) return { - 'row_cases_ordered': '' if row.removed else int(row.cases_ordered), - 'row_units_ordered': '' if row.removed else int(row.units_ordered), + 'row_cases_ordered': '' if row.removed else int(row.cases_ordered or 0), + 'row_units_ordered': '' if row.removed else int(row.units_ordered or 0), 'row_po_total': '' if row.removed else '${:0,.2f}'.format(row.po_total), 'batch_po_total': '${:0,.2f}'.format(batch.po_total), } @@ -689,7 +706,11 @@ class PurchaseBatchView(BatchMasterView): mode = form.data['mode'] shipped_product = form.data['product'] product = form.data['ordered_product'] if mode == 'mispick' else shipped_product - rows = [row for row in batch.active_rows() if row.product is product] + if product: + rows = [row for row in batch.active_rows() if row.product is product] + else: + upc = form.data['upc'] + rows = [row for row in batch.active_rows() if not row.product and row.upc == upc] if rows: if len(rows) > 1: log.warning("found {} matching rows in batch {} for product: {}".format( @@ -698,6 +719,11 @@ class PurchaseBatchView(BatchMasterView): else: row = model.PurchaseBatchRow() row.product = product + row.upc = form.data['upc'] + row.brand_name = form.data['brand_name'] + row.description = form.data['description'] + row.size = form.data['size'] + row.case_quantity = form.data['case_quantity'] batch.add_row(row) cases = form.data['cases'] @@ -716,9 +742,12 @@ class PurchaseBatchView(BatchMasterView): self.handler.refresh_row(row) + description = make_full_description(form.data['brand_name'], + form.data['description'], + form.data['size']) self.request.session.flash("({}) {} cases, {} units: {} {}".format( form.data['mode'], form.data['cases'] or 0, form.data['units'] or 0, - product.upc.pretty(), product)) + form.data['upc'].pretty(), description)) return self.redirect(self.request.current_route_url()) title = self.get_instance_title(batch) @@ -743,35 +772,78 @@ class PurchaseBatchView(BatchMasterView): 'error': "Current batch has already been executed", 'redirect': self.get_action_url('view', batch), } - data = None + data = {} upc = self.request.GET.get('upc', '').strip() upc = re.sub(r'\D', '', upc) if upc: - product = api.get_product_by_upc(Session(), upc) - if not product: - # Try again, assuming caller did not include check digit. - upc = GPC(upc, calc_check_digit='upc') - product = api.get_product_by_upc(Session(), upc) - if product and (not product.deleted or self.request.has_perm('products.view_deleted')): - data = { - 'uuid': product.uuid, - 'upc': unicode(product.upc), - 'upc_pretty': product.upc.pretty(), - 'full_description': product.full_description, - 'image_url': pod.get_image_url(self.rattail_config, product.upc), - } - cost = product.cost_for_vendor(batch.vendor) - if cost: - data['cost_found'] = True - if int(cost.case_size) == cost.case_size: - data['cost_case_size'] = int(cost.case_size) + + # first try to locate existing batch row by UPC match + provided = GPC(upc, calc_check_digit=False) + checked = GPC(upc, calc_check_digit='upc') + rows = Session.query(model.PurchaseBatchRow)\ + .filter(model.PurchaseBatchRow.batch == batch)\ + .filter(model.PurchaseBatchRow.upc.in_((provided, checked)))\ + .all() + if rows: + if len(rows) > 1: + log.warning("found multiple UPC matches for {} in batch {}: {}".format( + upc, batch.id_str, batch)) + row = rows[0] + data['uuid'] = row.product_uuid + data['upc'] = unicode(row.upc) + data['upc_pretty'] = row.upc.pretty() + data['full_description'] = make_full_description(row.brand_name, row.description, row.size) + data['brand_name'] = row.brand_name + data['description'] = row.description + data['size'] = row.size + data['case_quantity'] = pretty_quantity(row.case_quantity) + data['image_url'] = pod.get_image_url(self.rattail_config, row.upc) + data['found_in_batch'] = True + data['cases_ordered'] = pretty_quantity(row.cases_ordered, empty_zero=True) + data['units_ordered'] = pretty_quantity(row.units_ordered, empty_zero=True) + data['cases_received'] = pretty_quantity(row.cases_received, empty_zero=True) + data['units_received'] = pretty_quantity(row.units_received, empty_zero=True) + data['cases_damaged'] = pretty_quantity(row.cases_damaged, empty_zero=True) + data['units_damaged'] = pretty_quantity(row.units_damaged, empty_zero=True) + data['cases_expired'] = pretty_quantity(row.cases_expired, empty_zero=True) + data['units_expired'] = pretty_quantity(row.units_expired, empty_zero=True) + data['cases_mispick'] = pretty_quantity(row.cases_mispick, empty_zero=True) + data['units_mispick'] = pretty_quantity(row.units_mispick, empty_zero=True) + + else: # no match in our batch, do full product search + product = api.get_product_by_upc(Session(), provided) + if not product: + product = api.get_product_by_upc(Session(), checked) + if product and (not product.deleted or self.request.has_perm('products.view_deleted')): + data['uuid'] = product.uuid + data['upc'] = unicode(product.upc) + data['upc_pretty'] = product.upc.pretty() + data['full_description'] = product.full_description + data['brand_name'] = unicode(product.brand or '') + data['description'] = product.description + data['size'] = product.size + data['case_quantity'] = 1 # default + cost = product.cost_for_vendor(batch.vendor) + if cost: + data['cost_found'] = True + data['cost_case_size'] = pretty_quantity(cost.case_size) + data['case_quantity'] = pretty_quantity(cost.case_size) else: - data['cost_case_size'] = '{:0.4f}'.format(cost.case_size) - else: - data['cost_found'] = False - data['found_in_batch'] = product in [row.product for row in batch.active_rows()] - - return {'product': data} + data['cost_found'] = False + data['image_url'] = pod.get_image_url(self.rattail_config, product.upc) + data['found_in_batch'] = product in [row.product for row in batch.active_rows()] + + result = {'product': data or None, 'upc': None} + if not data and upc: + upc = GPC(upc) + result['upc'] = unicode(upc) + result['upc_pretty'] = upc.pretty() + result['image_url'] = pod.get_image_url(self.rattail_config, upc) + return result + + def mobile_index(self): + self.mobile = True + return self.render_to_response('mobile_index', {}) @classmethod def defaults(cls, config): @@ -781,6 +853,11 @@ class PurchaseBatchView(BatchMasterView): model_key = cls.get_model_key() model_title = cls.get_model_title() + # mobile + config.add_route('{}.mobile'.format(route_prefix), '/mobile{}'.format(url_prefix)) + config.add_view(cls, attr='mobile_index', route_name='{}.mobile'.format(route_prefix), + permission='{}.list'.format(permission_prefix)) + # eligible purchases (AJAX) config.add_route('{}.eligible_purchases'.format(route_prefix), '{}/eligible-purchases'.format(url_prefix)) config.add_view(cls, attr='eligible_purchases', route_name='{}.eligible_purchases'.format(route_prefix),