Add new 'receiving form' for purchase batches

This commit is contained in:
Lance Edgar 2016-12-08 17:24:42 -06:00
parent 468a84aa90
commit 6c3d221e98
5 changed files with 323 additions and 36 deletions

View file

@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2015 Lance Edgar
# Copyright © 2010-2016 Lance Edgar
#
# This file is part of Rattail.
#
@ -24,6 +24,8 @@
Forms
"""
from __future__ import unicode_literals, absolute_import
from formencode import Schema
from .core import Form, Field, FieldSet, GenericFieldSet

View file

@ -0,0 +1,147 @@
## -*- coding: utf-8 -*-
<%inherit file="/base.mako" />
<%def name="title()">Receiving Form (${batch.vendor})</%def>
<%def name="head_tags()">
${parent.head_tags()}
${h.javascript_link(request.static_url('tailbone:static/js/numeric.js'))}
<script type="text/javascript">
function invalid_product(msg) {
$('#product-info p').text(msg);
$('#product-info img').hide();
$('#product-info .rogue-item-warning').hide();
$('#product-textbox').focus().select();
}
$(function() {
$('#product-textbox').keydown(function(event) {
if (key_allowed(event)) {
return true;
}
if (key_modifies(event)) {
$('#product').val('');
$('#product-info p').html('&nbsp;');
$('#product-info img').hide();
$('#product-info .rogue-item-warning').hide();
return true;
}
if (event.which == 13) {
var input = $(this);
var data = {upc: input.val()};
$.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);
if (data.product.cost_found) {
$('#product').val(data.product.uuid);
$('#product-info p').text(data.product.full_description);
$('#product-info img').attr('src', data.product.image_url).show();
$('#product-info .rogue-item-warning').hide();
$('#cases').focus().select();
if (! data.product.found_in_batch) {
$('#product-info .rogue-item-warning').show();
}
} else {
invalid_product('cost not found for ' + data.product.full_description);
}
} else {
invalid_product('product not found');
}
});
}
return false;
});
$('#product-textbox').focus();
$('#received').click(function() {
$(this).button('disable').button('option', 'label', "Working...");
$('#mode').val('received');
$('#receiving-form').submit();
});
$('#receiving-form').submit(function() {
$(this).mask("Working...");
});
});
</script>
<style type="text/css">
#product-info {
margin-top: 0.5em;
text-align: center;
}
#product-info p {
margin-left: 0.5em;
}
#product-info .img-wrapper {
height: 150px;
margin: 0.5em 0;
}
#product-info .rogue-item-warning {
background: #f66;
display: none;
}
</style>
</%def>
<%def name="context_menu_items()">
<li>${h.link_to("Back to Purchase Batch", url('purchases.batch.view', uuid=batch.uuid))}</li>
</%def>
<ul id="context-menu">
${self.context_menu_items()}
</ul>
<div class="form-wrapper">
${form.begin(id='receiving-form')}
${h.hidden('mode')}
<div class="field-wrapper">
<label for="product-textbox">Product</label>
<div class="field">
${h.hidden('product')}
<div>${h.text('product-textbox', autocomplete='off')}</div>
<div id="product-info">
<p>&nbsp;</p>
<div class="img-wrapper"><img /></div>
<div class="rogue-item-warning">warning: product not found on current purchase</div>
</div>
</div>
</div>
<div class="field-wrapper">
<label for="cases">Cases</label>
<div class="field">${h.text('cases')}</div>
</div>
<div class="field-wrapper">
<label for="units">Units</label>
<div class="field">${h.text('units')}</div>
</div>
<div class="buttons">
<button type="button" id="received">Received</button>
<button type="button" id="damaged" disabled="disabled">Damaged</button>
<button type="button" id="expired" disabled="disabled">Expired</button>
<button type="button" id="mispick" disabled="disabled">Mispick</button>
</div>
${form.end()}
</div>

View file

@ -18,13 +18,20 @@
location.href = '${url('purchases.batch.order_form', uuid=batch.uuid)}';
});
$('#receive-form').click(function() {
$(this).button('disable').button('option', 'label', "Working, please wait...");
location.href = '${url('purchases.batch.receiving_form', uuid=batch.uuid)}';
});
});
</script>
</%def>
<%def name="leading_buttons()">
% if batch.mode == enum.PURCHASE_BATCH_MODE_NEW and not batch.complete and not batch.executed and request.has_perm('purchases.batch.order_form'):
<button type="button" id="order-form">View as Order Form</button>
<button type="button" id="order-form">Ordering Form</button>
% elif batch.mode == enum.PURCHASE_BATCH_MODE_RECEIVING and not batch.complete and not batch.executed and request.has_perm('purchases.batch.receiving_form'):
<button type="button" id="receive-form">Receiving Form</button>
% endif
</%def>

View file

@ -335,6 +335,46 @@ class ProductsView(MasterView):
'instance_title': self.get_instance_title(instance),
'form': form})
def search(self):
"""
Locate a product(s) by UPC.
Eventually this should be more generic, or at least offer more fields for
search. For now it operates only on the ``Product.upc`` field.
"""
data = None
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),
}
uuid = self.request.GET.get('with_vendor_cost')
if uuid:
vendor = Session.query(model.Vendor).get(uuid)
if not vendor:
return {'error': "Vendor not found"}
cost = product.cost_for_vendor(vendor)
if cost:
data['cost_found'] = True
if int(cost.case_size) == cost.case_size:
data['cost_case_size'] = int(cost.case_size)
else:
data['cost_case_size'] = '{:0.4f}'.format(cost.case_size)
else:
data['cost_found'] = False
return {'product': data}
def get_supported_batches(self):
return {
'labels': 'rattail.batch.labels:LabelBatchHandler',
@ -479,6 +519,11 @@ class ProductsView(MasterView):
config.add_view(cls, attr='make_batch', route_name='products.create_batch',
renderer='/products/batch.mako', permission='batches.create')
# search (by upc)
config.add_route('products.search', '/products/search')
config.add_view(cls, attr='search', route_name='products.search',
renderer='json', permission='products.view')
cls._defaults(config)
@ -535,34 +580,6 @@ class ProductsAutocomplete(AutocompleteView):
return product.full_description
def products_search(request):
"""
Locate a product(s) by UPC.
Eventually this should be more generic, or at least offer more fields for
search. For now it operates only on the ``Product.upc`` field.
"""
product = None
upc = 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:
if product.deleted and not request.has_perm('products.view_deleted'):
product = None
else:
product = {
'uuid': product.uuid,
'upc': unicode(product.upc or ''),
'full_description': product.full_description,
}
return {'product': product}
def print_labels(request):
profile = request.params.get('profile')
profile = Session.query(model.LabelProfile).get(profile) if profile else None
@ -600,9 +617,5 @@ def includeme(config):
config.add_view(print_labels, route_name='products.print_labels',
renderer='json', permission='products.print_labels')
config.add_route('products.search', '/products/search')
config.add_view(products_search, route_name='products.search',
renderer='json', permission='products.list')
ProductsView.defaults(config)
version_defaults(config, ProductVersionView, 'product')

View file

@ -26,9 +26,12 @@ Views for purchase order batches
from __future__ import unicode_literals, absolute_import
import re
import logging
from sqlalchemy import orm
from rattail import enum
from rattail import enum, pod
from rattail.db import model, api
from rattail.gpc import GPC
from rattail.time import localtime
@ -36,6 +39,7 @@ from rattail.core import Object
from rattail.util import OrderedDict
import formalchemy as fa
import formencode as fe
from pyramid import httpexceptions
from tailbone import forms
@ -43,6 +47,21 @@ from tailbone.db import Session
from tailbone.views.batch import BatchMasterView
log = logging.getLogger(__name__)
class ReceivingForm(forms.Schema):
allow_extra_fields = True
filter_extra_fields = True
mode = fe.validators.OneOf([
'received',
# 'damaged', 'expired', 'mispick',
])
product = forms.validators.ValidProduct()
cases = fe.validators.Int()
units = fe.validators.Int()
class PurchaseBatchView(BatchMasterView):
"""
Master view for purchase order batches.
@ -578,6 +597,95 @@ class PurchaseBatchView(BatchMasterView):
'batch_po_total': '${:0,.2f}'.format(batch.po_total),
}
def receiving_form(self):
"""
Workflow view for receiving items on a purchase batch.
"""
batch = self.get_instance()
if batch.executed:
return self.redirect(self.get_action_url('view', batch))
form = forms.SimpleForm(self.request, schema=ReceivingForm)
if form.validate():
assert form.data['mode'] == 'received' # TODO
product = form.data['product']
rows = [row for row in batch.active_rows() if row.product is product]
if rows:
if len(rows) > 1:
log.warning("found {} matching rows in batch {} for product: {}".format(
len(rows), batch.id_str, product.upc.pretty()))
row = rows[0]
else:
row = model.PurchaseBatchRow()
row.product = product
if form.data['cases']:
row.cases_received = (row.cases_received or 0) + form.data['cases']
if form.data['units']:
row.units_received = (row.units_received or 0) + form.data['units']
if not row.uuid:
batch.add_row(row)
self.handler.refresh_row(row)
self.request.session.flash("({}) {} cases, {} units: {} {}".format(
form.data['mode'], form.data['cases'] or 0, form.data['units'] or 0,
product.upc.pretty(), product))
return self.redirect(self.request.current_route_url())
title = self.get_instance_title(batch)
return self.render_to_response('receive_form', {
'batch': batch,
'instance': batch,
'instance_title': title,
'index_title': "{}: {}".format(self.get_model_title(), title),
'index_url': self.get_action_url('view', batch),
'vendor': batch.vendor,
'form': forms.FormRenderer(form),
})
def receiving_lookup(self):
"""
Try to locate a product by UPC, and validate it in the context of
current batch, returning some data for client JS.
"""
batch = self.get_instance()
if batch.executed:
return {
'error': "Current batch has already been executed",
'redirect': self.get_action_url('view', batch),
}
data = None
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)
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}
@classmethod
def defaults(cls, config):
route_prefix = cls.get_route_prefix()
@ -595,7 +703,7 @@ class PurchaseBatchView(BatchMasterView):
cls._batch_defaults(config)
cls._defaults(config)
# order form
# ordering form
config.add_tailbone_permission(permission_prefix, '{}.order_form'.format(permission_prefix),
"Edit new {} in Order Form mode".format(model_title))
config.add_route('{}.order_form'.format(route_prefix), '{}/{{{}}}/order-form'.format(url_prefix, model_key))
@ -605,6 +713,16 @@ class PurchaseBatchView(BatchMasterView):
config.add_view(cls, attr='order_form_update', route_name='{}.order_form_update'.format(route_prefix),
renderer='json', permission='{}.order_form'.format(permission_prefix))
# receiving form, lookup
config.add_tailbone_permission(permission_prefix, '{}.receiving_form'.format(permission_prefix),
"Edit 'receiving' {} in Receiving Form mode".format(model_title))
config.add_route('{}.receiving_form'.format(route_prefix), '{}/{{{}}}/receiving-form'.format(url_prefix, model_key))
config.add_view(cls, attr='receiving_form', route_name='{}.receiving_form'.format(route_prefix),
permission='{}.receiving_form'.format(permission_prefix))
config.add_route('{}.receiving_lookup'.format(route_prefix), '{}/{{{}}}/receiving-form/lookup'.format(url_prefix, model_key))
config.add_view(cls, attr='receiving_lookup', route_name='{}.receiving_lookup'.format(route_prefix),
renderer='json', permission='{}.receiving_form'.format(permission_prefix))
def includeme(config):
PurchaseBatchView.defaults(config)