Add support for variance inventory batches, aggregation by product

kind of a rushed job but hopefully this is all good...
This commit is contained in:
Lance Edgar 2018-06-01 12:49:01 -05:00
parent 5bc4a1618b
commit db645fb393
2 changed files with 127 additions and 38 deletions

View file

@ -101,7 +101,13 @@
$('#description').val(data.product.description); $('#description').val(data.product.description);
$('#size').val(data.product.size); $('#size').val(data.product.size);
$('#case_quantity').val(data.product.case_quantity); $('#case_quantity').val(data.product.case_quantity);
if (data.product.type2) {
if (data.already_present_in_batch) {
$('#product-info .warning.present').show();
$('#cases').val(data.cases);
$('#units').val(data.units);
} else if (data.product.type2) {
$('#units').val(data.product.units); $('#units').val(data.product.units);
} }
@ -117,11 +123,18 @@
% endif % endif
$('.field-wrapper.units input').prop('disabled', false); $('.field-wrapper.units input').prop('disabled', false);
$('.buttons button').button('enable'); $('.buttons button').button('enable');
if (data.product.type2) { if (data.product.type2) {
$('#units').focus().select(); $('#units').focus().select();
} else { } else {
% if allow_cases: % if allow_cases:
$('#cases').focus().select(); if ($('#cases').val()) {
$('#cases').focus().select();
} else if ($('#units').val()) {
$('#units').focus().select();
} else {
$('#cases').focus().select();
}
% else: % else:
$('#units').focus().select(); $('#units').focus().select();
% endif % endif
@ -222,6 +235,7 @@
<p>please ENTER a scancode</p> <p>please ENTER a scancode</p>
<div class="img-wrapper"><img /></div> <div class="img-wrapper"><img /></div>
<div class="warning notfound">please confirm UPC and provide more details</div> <div class="warning notfound">please confirm UPC and provide more details</div>
<div class="warning present">product already exists in batch, please confirm count</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -27,6 +27,7 @@ Views for inventory batches
from __future__ import unicode_literals, absolute_import from __future__ import unicode_literals, absolute_import
import re import re
import decimal
import logging import logging
import six import six
@ -63,6 +64,10 @@ class InventoryAdjustmentReasonsView(MasterView):
'description', 'description',
] ]
def configure_grid(self, g):
super(InventoryAdjustmentReasonsView, self).configure_grid(g)
g.set_sort_defaults('code')
def configure_form(self, f): def configure_form(self, f):
super(InventoryAdjustmentReasonsView, self).configure_form(f) super(InventoryAdjustmentReasonsView, self).configure_form(f)
@ -296,17 +301,28 @@ class InventoryBatchView(BatchMasterView):
if form.validate(newstyle=True): if form.validate(newstyle=True):
product = self.Session.query(model.Product).get(form.validated['product']) product = self.Session.query(model.Product).get(form.validated['product'])
row = model.InventoryBatchRow()
row.product = product row = None
row.upc = form.validated['upc'] if self.should_aggregate_products(batch):
row.brand_name = form.validated['brand_name'] row = self.find_row_for_product(batch, product)
row.description = form.validated['description'] if row:
row.size = form.validated['size'] row.cases = form.validated['cases']
row.case_quantity = form.validated['case_quantity'] row.units = form.validated['units']
row.cases = form.validated['cases'] self.handler.refresh_row(row)
row.units = form.validated['units']
self.handler.capture_current_units(row) if not row:
self.handler.add_row(batch, row) row = model.InventoryBatchRow()
row.product = product
row.upc = form.validated['upc']
row.brand_name = form.validated['brand_name']
row.description = form.validated['description']
row.size = form.validated['size']
row.case_quantity = form.validated['case_quantity']
row.cases = form.validated['cases']
row.units = form.validated['units']
self.handler.capture_current_units(row)
self.handler.add_row(batch, row)
description = make_full_description(form.validated['brand_name'], description = make_full_description(form.validated['brand_name'],
form.validated['description'], form.validated['description'],
form.validated['size']) form.validated['size'])
@ -334,6 +350,15 @@ class InventoryBatchView(BatchMasterView):
return False return False
return True return True
def should_aggregate_products(self, batch):
"""
Must return a boolean indicating whether rows should be aggregated by
product for the given batch.
"""
if batch.mode == self.enum.INVENTORY_MODE_VARIANCE:
return True
return False
def desktop_lookup(self): def desktop_lookup(self):
""" """
Try to locate a product by UPC, and validate it in the context of Try to locate a product by UPC, and validate it in the context of
@ -345,12 +370,25 @@ class InventoryBatchView(BatchMasterView):
'error': "Current batch has already been executed", 'error': "Current batch has already been executed",
'redirect': self.get_action_url('view', batch), 'redirect': self.get_action_url('view', batch),
} }
data = {}
entry = self.request.GET.get('upc', '') entry = self.request.GET.get('upc', '')
product = self.find_product(entry) aggregate = self.should_aggregate_products(batch)
data = self.product_info(product)
result = {'product': data or None, 'upc_raw': entry, 'upc': None} type2 = self.find_type2_product(entry)
if type2:
product, price = type2
else:
product = self.find_product(entry)
data = self.product_info(product)
if type2:
data['type2'] = True
if not aggregate:
if price is None:
data['units'] = 1
else:
data['units'] = float((price / product.regular_price.price).quantize(decimal.Decimal('0.01')))
result = {'product': data, 'upc_raw': entry, 'upc': None}
if not data: if not data:
upc = re.sub(r'\D', '', entry.strip()) upc = re.sub(r'\D', '', entry.strip())
if upc: if upc:
@ -358,8 +396,28 @@ class InventoryBatchView(BatchMasterView):
result['upc'] = six.text_type(upc) result['upc'] = six.text_type(upc)
result['upc_pretty'] = upc.pretty() result['upc_pretty'] = upc.pretty()
result['image_url'] = pod.get_image_url(self.rattail_config, upc) result['image_url'] = pod.get_image_url(self.rattail_config, upc)
if product and aggregate:
row = self.find_row_for_product(batch, product)
if row:
result['already_present_in_batch'] = True
result['cases'] = float(row.cases) if row.cases is not None else None
result['units'] = float(row.units) if row.units is not None else None
return result return result
def find_row_for_product(self, batch, product):
rows = self.Session.query(model.InventoryBatchRow)\
.filter(model.InventoryBatchRow.batch == batch)\
.filter(model.InventoryBatchRow.product == product)\
.filter(model.InventoryBatchRow.removed == False)\
.all()
if rows:
if len(rows) > 1:
log.error("inventory batch %s should aggregate products, but has %s rows for: %s",
batch.id_str, len(rows), product)
return rows[0]
def find_product(self, entry): def find_product(self, entry):
upc = re.sub(r'\D', '', entry.strip()) upc = re.sub(r'\D', '', entry.strip())
if upc: if upc:
@ -426,41 +484,58 @@ class InventoryBatchView(BatchMasterView):
""" """
batch = self.get_instance() batch = self.get_instance()
row = None row = None
upc = self.request.GET.get('upc', '').strip() entry = self.request.GET.get('upc', '').strip()
upc = re.sub(r'\D', '', upc) entry = re.sub(r'\D', '', entry)
if upc: if entry:
if len(upc) <= 14: if len(entry) <= 14:
row = self.add_row_for_upc(batch, upc) row = self.add_row_for_upc(batch, entry, warn_if_present=True)
if not row: if not row:
self.request.session.flash("Product not found: {}".format(upc), 'error') self.request.session.flash("Product not found: {}".format(entry), 'error')
return self.redirect(self.get_action_url('view', batch, mobile=True)) return self.redirect(self.get_action_url('view', batch, mobile=True))
else: else:
self.request.session.flash("UPC has too many digits ({}): {}".format(len(upc), upc), 'error') self.request.session.flash("UPC has too many digits ({}): {}".format(len(entry), entry), 'error')
return self.redirect(self.get_action_url('view', batch, mobile=True)) return self.redirect(self.get_action_url('view', batch, mobile=True))
self.Session.flush() self.Session.flush()
return self.redirect(self.mobile_row_route_url('view', uuid=row.batch_uuid, row_uuid=row.uuid)) return self.redirect(self.mobile_row_route_url('view', uuid=row.batch_uuid, row_uuid=row.uuid))
def add_row_for_upc(self, batch, upc): def add_row_for_upc(self, batch, entry, warn_if_present=False):
""" """
Add a row to the batch for the given UPC, if applicable. Add a row to the batch for the given UPC, if applicable.
""" """
# try to locate general product by UPC; add to batch either way type2 = self.find_type2_product(entry)
provided = GPC(upc, calc_check_digit=False) if type2:
checked = GPC(upc, calc_check_digit='upc') product, price = type2
product = api.get_product_by_upc(self.Session(), provided) else:
if not product: product = self.find_product(entry)
product = api.get_product_by_upc(self.Session(), checked) if product:
if product or self.unknown_product_creates_row:
aggregate = self.should_aggregate_products(batch)
if aggregate:
row = self.find_row_for_product(batch, product)
if row:
if warn_if_present:
self.request.session.flash("Product already exists in batch; please confirm counts", 'error')
return row
row = model.InventoryBatchRow() row = model.InventoryBatchRow()
if product: row.product = product
row.product = product row.upc = product.upc
row.upc = product.upc self.handler.capture_current_units(row)
else: if type2 and not aggregate:
row.upc = provided # TODO: why not 'checked' instead? how to choose? if price is None:
row.description = "(unknown product)" row.units = 1
else:
row.units = (price / product.regular_price.price).quantize(decimal.Decimal('0.01'))
self.handler.add_row(batch, row)
return row
elif self.unknown_product_creates_row:
row = model.InventoryBatchRow()
row.upc = GPC(upc, calc_check_digit=False) # TODO: why not calc check digit?
row.description = "(unknown product)"
self.handler.capture_current_units(row) self.handler.capture_current_units(row)
self.handler.add_row(batch, row) self.handler.add_row(batch, row)
return row return row