Add initial support for receiving truck dump batch via mobile

i.e. just the initial truck dump, but secondary invoice batches are not yet
supported.  also this maybe breaks other things..we'll see
This commit is contained in:
Lance Edgar 2018-05-16 09:15:52 -05:00
parent b515331e48
commit 9ed501a8cc
9 changed files with 214 additions and 62 deletions

View file

@ -153,6 +153,13 @@ class EmployeeType(ModelType):
model_class = model.Employee
class VendorType(ModelType):
"""
Custom schema type for vendor relationship field.
"""
model_class = model.Vendor
class ProductType(ModelType):
"""
Custom schema type for product relationship field.

View file

@ -56,10 +56,12 @@
// when user clicks autocomplete result, hide search etc.
this.ul.on('click', 'li', function() {
var $li = $(this);
var uuid = $li.data('uuid');
that.search.hide();
that.hidden_field.val($li.data('uuid'));
that.hidden_field.val(uuid);
that.button.text($li.text()).show();
that.ul.hide();
that.element.trigger('autocompleteitemselected', uuid);
});
// when user clicks "change" button, show search etc.
@ -69,6 +71,7 @@
that.hidden_field.val('');
that.search.show();
that.text_field.focus();
that.element.trigger('autocompleteitemcleared');
});
}

View file

@ -0,0 +1,34 @@
/************************************************************
*
* tailbone.mobile.receiving.js
*
* Global logic for mobile receiving feature
*
************************************************************/
// TODO: this is really just for receiving; should change form name?
$(document).on('autocompleteitemselected', 'form[name="new-purchasing-batch"] .vendor', function(event, uuid) {
$('#new-receiving-types').show();
});
// TODO: this is really just for receiving; should change form name?
$(document).on('autocompleteitemcleared', 'form[name="new-purchasing-batch"] .vendor', function(event) {
$('#new-receiving-types').hide();
});
$(document).on('click', 'form[name="new-purchasing-batch"] #receive-truck-dump', function() {
var form = $(this).parents('form');
form.find('input[name="workflow"]').val('truck_dump');
form.submit();
});
$(document).on('click', 'form.receiving-update #delete-receiving-row', function() {
var form = $(this).parents('form');
form.find('input[name="delete_row"]').val('true');
form.submit();
});

View file

@ -75,7 +75,7 @@
${rows_grid|n}
% if not batch.executed:
% if master.handler.executable(batch) and not batch.executed:
<div id="execution-options-dialog" style="display: none;">
${execute_form.render_deform(form_kwargs={'name': 'batch-execution'}, buttons=False)|n}
</div>

View file

@ -10,6 +10,7 @@
${h.javascript_link('https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js')}
${h.javascript_link(request.static_url('tailbone:static/js/jquery.ui.tailbone.mobile.js'))}
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.mobile.js'))}
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.mobile.receiving.js'))}
${self.extra_javascript()}
## since jquery mobile will "utterly cache" the first page which is loaded

View file

@ -20,8 +20,25 @@ ${h.csrf_token(request)}
</div>
<br />
${h.submit('submit', "Find purchase orders")}
## <button type="button">New receiving from scratch</button>
<div id="new-receiving-types" style="display: none;">
${h.hidden('workflow')}
% if master.allow_from_po:
## ${h.submit('submit', "Find purchase orders")}
<button type="button">Receive from PO</button>
% endif
% if master.allow_from_scratch:
<button type="button">Receive from Scratch</button>
% endif
% if master.allow_truck_dump:
<button type="button" id="receive-truck-dump">Receive Truck Dump</button>
% endif
</div>
% else: ## vendor is known

View file

@ -2,9 +2,9 @@
<%inherit file="/mobile/master/view_row.mako" />
<%namespace file="/mobile/keypad.mako" import="keypad" />
<%def name="title()">Receiving &raquo; ${instance.batch.id_str} &raquo; ${row.upc.pretty()}</%def>
<%def name="title()">Receiving &raquo; ${batch.id_str} &raquo; ${row.upc.pretty()}</%def>
<%def name="page_title()">${h.link_to("Receiving", url('mobile.receiving'))} &raquo; ${h.link_to(instance.batch.id_str, url('mobile.receiving.view', uuid=instance.batch_uuid))} &raquo; ${row.upc.pretty()}</%def>
<%def name="page_title()">${h.link_to("Receiving", url('mobile.receiving'))} &raquo; ${h.link_to(batch.id_str, url('mobile.receiving.view', uuid=batch.uuid))} &raquo; ${row.upc.pretty()}</%def>
<%
unit_uom = 'LB' if row.product and row.product.weighed else 'EA'
@ -20,7 +20,7 @@
% if instance.product:
<h3>${instance.brand_name or ""}</h3>
<h3>${instance.description} ${instance.size}</h3>
<h3>${h.pretty_quantity(row.case_quantity)} ${unit_uom} per CS</h3>
<h3>1 CS = ${h.pretty_quantity(row.case_quantity)} ${unit_uom}</h3>
% else:
<h3>${instance.description}</h3>
% endif
@ -32,10 +32,12 @@
<table>
<tbody>
% if not batch.truck_dump:
<tr>
<td>ordered</td>
<td>${h.pretty_quantity(row.cases_ordered or 0)} / ${h.pretty_quantity(row.units_ordered or 0)}</td>
</tr>
% endif
<tr>
<td>received</td>
<td>${h.pretty_quantity(row.cases_received or 0)} / ${h.pretty_quantity(row.units_received or 0)}</td>
@ -57,7 +59,7 @@
% endfor
% endif
% if not instance.batch.executed and not instance.batch.complete:
% if not batch.executed and not batch.complete:
${h.form(request.current_route_url(), class_='receiving-update')}
${h.csrf_token(request)}
@ -98,5 +100,10 @@
</tbody>
</table>
${h.hidden('delete_row', value='false')}
% if request.has_perm('{}.delete_row'.format(permission_prefix)):
<button type="button" id="delete-receiving-row">Delete this Row</button>
% endif
${h.end_form()}
% endif

View file

@ -512,6 +512,7 @@ class PurchasingBatchView(BatchMasterView):
def get_batch_kwargs(self, batch, mobile=False):
kwargs = super(PurchasingBatchView, self).get_batch_kwargs(batch, mobile=mobile)
kwargs['mode'] = self.batch_mode
kwargs['truck_dump'] = batch.truck_dump
if batch.store:
kwargs['store'] = batch.store
elif batch.store_uuid:

View file

@ -36,6 +36,8 @@ from rattail.gpc import GPC
from rattail.util import pretty_quantity, prettify
import colander
from deform import widget as dfwidget
from pyramid import httpexceptions
from webhelpers2.html import tags
from tailbone import forms, grids
@ -48,6 +50,12 @@ class MobileItemStatusFilter(grids.filters.MobileFilter):
def filter_equal(self, query, value):
# NOTE: this is only relevant for truck dump
if value == 'received':
return query.filter(sa.or_(
model.PurchaseBatchRow.cases_received != 0,
model.PurchaseBatchRow.units_received != 0))
# TODO: is this accurate (enough) ?
if value == 'incomplete':
return query.filter(sa.or_(model.PurchaseBatchRow.cases_ordered != 0, model.PurchaseBatchRow.units_ordered != 0))\
@ -95,10 +103,29 @@ class ReceivingBatchView(PurchasingBatchView):
mobile_rows_filterable = True
mobile_rows_creatable = True
allow_from_po = False
allow_from_scratch = True
allow_truck_dump = False
grid_columns = [
'id',
'vendor',
'truck_dump',
'department',
'buyer',
'date_ordered',
'created',
'created_by',
'rowcount',
'status_code',
'executed',
]
form_fields = [
'id',
'store',
'vendor',
'truck_dump',
'department',
'purchase',
'vendor_email',
@ -123,6 +150,7 @@ class ReceivingBatchView(PurchasingBatchView):
mobile_form_fields = [
'vendor',
'truck_dump',
'department',
]
@ -175,6 +203,13 @@ class ReceivingBatchView(PurchasingBatchView):
def batch_mode(self):
return self.enum.PURCHASE_BATCH_MODE_RECEIVING
def configure_form(self, f):
super(ReceivingBatchView, self).configure_form(f)
# truck_dump
if self.editing:
f.set_readonly('truck_dump')
def render_mobile_listitem(self, batch, i):
title = "({}) {} for ${:0,.2f} - {}, {}".format(
batch.id_str,
@ -188,8 +223,17 @@ class ReceivingBatchView(PurchasingBatchView):
"""
Returns a set of filters for the mobile row grid.
"""
batch = self.get_instance()
filters = grids.filters.GridFilterSet()
filters['status'] = MobileItemStatusFilter('status', default_value='incomplete')
if batch.truck_dump:
value_choices = ['received', 'damaged', 'expired', 'all']
default_status = 'all'
else:
value_choices = ['incomplete', 'unexpected', 'damaged', 'expired', 'all']
default_status = 'incomplete'
filters['status'] = MobileItemStatusFilter('status',
value_choices=value_choices,
default_value=default_status)
return filters
def mobile_create(self):
@ -199,6 +243,25 @@ class ReceivingBatchView(PurchasingBatchView):
mode = self.batch_mode
data = {'mode': mode}
form = forms.Form(schema=MobileNewReceivingBatch(), request=self.request)
if form.validate(newstyle=True):
if form.validated['workflow'] == 'truck_dump':
if not self.allow_truck_dump:
raise NotImplementedError("Requested workflow not supported: truck_dump")
batch = self.model_class()
batch.store = self.rattail_config.get_store(self.Session())
batch.mode = mode
batch.truck_dump = True
batch.vendor = self.Session.merge(form.validated['vendor'])
batch.created_by = self.request.user
kwargs = self.get_batch_kwargs(batch, mobile=True)
batch = self.handler.make_batch(self.Session(), **kwargs)
return self.redirect(self.request.route_url('mobile.receiving.view', uuid=batch.uuid))
else:
raise NotImplementedError("Requested workflow not supported: {}".format(form.validated['workflow']))
vendor = None
if self.request.method == 'POST' and self.request.POST.get('vendor'):
vendor = self.Session.query(model.Vendor).get(self.request.POST['vendor'])
@ -227,28 +290,19 @@ class ReceivingBatchView(PurchasingBatchView):
data['purchases'] = [(p['key'], p['display']) for p in purchases['purchases']]
return self.render_to_response('create', data, mobile=True)
def get_batch_kwargs(self, batch, mobile=False):
kwargs = super(ReceivingBatchView, self).get_batch_kwargs(batch, mobile=mobile)
if mobile:
purchase = self.get_purchase(self.request.POST['purchase'])
numbers = [d.F03 for d in purchase.details]
if numbers:
number = max(set(numbers), key=numbers.count)
kwargs['department'] = self.Session.query(model.Department)\
.filter(model.Department.number == number)\
.one()
return kwargs
def configure_mobile_form(self, f):
super(ReceivingBatchView, self).configure_mobile_form(f)
batch = f.model_instance
# vendor
# fs.vendor.with_renderer(fa.TextFieldRenderer),
# truck_dump
if not self.creating:
if not batch.truck_dump:
f.remove_field('truck_dump')
# department
# fs.department.with_renderer(fa.TextFieldRenderer),
if not self.creating:
if batch.truck_dump:
f.remove_field('department')
def configure_row_form(self, f):
super(ReceivingBatchView, self).configure_row_form(f)
@ -302,8 +356,7 @@ class ReceivingBatchView(PurchasingBatchView):
if product:
row = model.PurchaseBatchRow()
row.product = product
batch.add_row(row)
self.handler.refresh_row(row)
self.handler.add_row(batch, row)
# check for "bad" upc
elif len(upc) > 14:
@ -329,9 +382,12 @@ class ReceivingBatchView(PurchasingBatchView):
"""
self.viewing = True
row = self.get_row_instance()
batch = row.batch
permission_prefix = self.get_permission_prefix()
form = self.make_mobile_row_form(row)
context = {
'row': row,
'batch': batch,
'instance': row,
'instance_title': self.get_row_instance_title(row),
'parent_model_title': self.get_model_title(),
@ -339,10 +395,19 @@ class ReceivingBatchView(PurchasingBatchView):
'form': form,
}
if self.request.has_perm('{}.create_row'.format(self.get_permission_prefix())):
update_form = forms.Form(schema=ReceivingForm(), request=self.request)
if self.request.has_perm('{}.create_row'.format(permission_prefix)):
update_form = forms.Form(schema=MobileReceivingForm(), request=self.request)
if update_form.validate(newstyle=True):
row = self.Session.merge(update_form.validated['row'])
# TODO: surely this (delete_row) should be split out to a separate view
if update_form.validated['delete_row']:
if not self.request.has_perm('{}.delete_row'.format(permission_prefix)):
raise httpexceptions.HTTPForbidden()
self.handler.remove_row(row)
return self.redirect(self.get_action_url('view', batch, mobile=True))
else: # not delete_row
mode = update_form.validated['mode']
cases = update_form.validated['cases']
units = update_form.validated['units']
@ -363,12 +428,12 @@ class ReceivingBatchView(PurchasingBatchView):
# first undo any totals previously in effect for the row, then refresh
if row.invoice_total:
row.batch.invoice_total -= row.invoice_total
batch.invoice_total -= row.invoice_total
self.handler.refresh_row(row)
return self.redirect(self.request.route_url('mobile.{}.view'.format(self.get_route_prefix()), uuid=row.batch_uuid))
return self.redirect(self.get_action_url('view', batch, mobile=True))
if not row.cases_ordered and not row.units_ordered:
if not row.cases_ordered and not row.units_ordered and not batch.truck_dump:
self.request.session.flash("This item was NOT on the original purchase order.", 'receiving-warning')
return self.render_to_response('view_row', context, mobile=True)
@ -438,12 +503,25 @@ class PurchaseBatchRowType(forms.types.ObjectType):
return row
class ReceivingForm(colander.MappingSchema):
class MobileNewReceivingBatch(colander.MappingSchema):
vendor = colander.SchemaNode(forms.types.VendorType())
workflow = colander.SchemaNode(colander.String(),
validator=colander.OneOf([
'from_po',
'from_scratch',
'truck_dump',
]))
class MobileReceivingForm(colander.MappingSchema):
row = colander.SchemaNode(PurchaseBatchRowType())
mode = colander.SchemaNode(colander.String(),
validator=colander.OneOf(['received',
validator=colander.OneOf([
'received',
'damaged',
'expired',
# 'mispick',
@ -453,7 +531,11 @@ class ReceivingForm(colander.MappingSchema):
units = colander.SchemaNode(colander.Decimal(), missing=None)
expiration_date = colander.SchemaNode(colander.Date(), missing=colander.null)
expiration_date = colander.SchemaNode(colander.Date(),
widget=dfwidget.TextInputWidget(),
missing=colander.null)
delete_row = colander.SchemaNode(colander.Boolean())
def includeme(config):