Add (restore?) basic support for mobile receiving from PO

This commit is contained in:
Lance Edgar 2018-07-18 16:25:54 -05:00
parent 87ba8026e5
commit 34bdd2ac84
5 changed files with 122 additions and 67 deletions

View file

@ -57,7 +57,7 @@ function setfocus() {
var queries = [ var queries = [
'#username', '#username',
'#new-purchasing-batch-vendor-text', '#new-purchasing-batch-vendor-text',
// '.receiving-upc-search', '#new-receiving-batch-vendor-text',
]; ];
$.each(queries, function(i, query) { $.each(queries, function(i, query) {
el = $(query); el = $(query);
@ -92,16 +92,6 @@ $(document).on('click', 'form[name="new-purchasing-batch"] input[type="submit"]'
} }
}); });
// submit new purchasing batch form on Purchase click
$(document).on('click', 'form[name="new-purchasing-batch"] [data-role="listview"] a', function() {
var $form = $(this).parents('form');
var $field = $form.find('[name="purchase"]');
var uuid = $(this).parents('li').data('uuid');
$field.val(uuid);
$form.submit();
return false;
});
// disable datasync restart button when clicked // disable datasync restart button when clicked
$(document).on('click', '#datasync-restart', function() { $(document).on('click', '#datasync-restart', function() {

View file

@ -8,29 +8,31 @@
************************************************************/ ************************************************************/
// TODO: this is really just for receiving; should change form name? // toggle visibility of "Receive" type buttons based on whether vendor is set
$(document).on('autocompleteitemselected', 'form[name="new-purchasing-batch"] .vendor', function(event, uuid) { $(document).on('autocompleteitemselected', 'form[name="new-receiving-batch"] .vendor', function(event, uuid) {
$('#new-receiving-types').show(); $('#new-receiving-types').show();
}); });
$(document).on('autocompleteitemcleared', 'form[name="new-receiving-batch"] .vendor', function(event) {
// 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(); $('#new-receiving-types').hide();
}); });
$(document).on('click', 'form[name="new-purchasing-batch"] #receive-truck-dump', function() { // submit new receiving batch form when user clicks "Receive" type button
$(document).on('click', 'form[name="new-receiving-batch"] .start-receiving', function() {
var form = $(this).parents('form'); var form = $(this).parents('form');
form.find('input[name="workflow"]').val('truck_dump'); form.find('input[name="workflow"]').val($(this).data('workflow'));
form.submit(); form.submit();
}); });
$(document).on('click', 'form[name="new-purchasing-batch"] #receive-from-scratch', function() { // submit new receiving batch form when user clicks Purchase Order option
$(document).on('click', 'form[name="new-receiving-batch"] [data-role="listview"] a', function() {
var form = $(this).parents('form'); var form = $(this).parents('form');
form.find('input[name="workflow"]').val('from_scratch'); var key = $(this).parents('li').data('key');
form.find('[name="workflow"]').val('from_po');
form.find('.purchase-order-field').val(key);
form.submit(); form.submit();
return false;
}); });

View file

@ -5,16 +5,16 @@
<%def name="page_title()">${h.link_to("Receiving", url('mobile.receiving'))} &raquo; New Batch</%def> <%def name="page_title()">${h.link_to("Receiving", url('mobile.receiving'))} &raquo; New Batch</%def>
${h.form(request.current_route_url(), class_='ui-filterable', name='new-purchasing-batch')} ${h.form(form.action_url, class_='ui-filterable', name='new-receiving-batch')}
${h.csrf_token(request)} ${h.csrf_token(request)}
% if vendor is Undefined: % if phase == 1:
<div class="field-wrapper vendor"> <div class="field-wrapper vendor">
<div class="field autocomplete" data-url="${url('vendors.autocomplete')}"> <div class="field autocomplete" data-url="${url('vendors.autocomplete')}">
${h.hidden('vendor')} ${h.hidden('vendor')}
${h.text('new-purchasing-batch-vendor-text', placeholder="Vendor name", autocomplete='off', **{'data-type': 'search'})} ${h.text('new-receiving-batch-vendor-text', placeholder="Vendor name", autocomplete='off', **{'data-type': 'search'})}
<ul data-role="listview" data-inset="true" data-filter="true" data-input="#new-purchasing-batch-vendor-text"></ul> <ul data-role="listview" data-inset="true" data-filter="true" data-input="#new-receiving-batch-vendor-text"></ul>
<button type="button" style="display: none;">Change Vendor</button> <button type="button" style="display: none;">Change Vendor</button>
</div> </div>
</div> </div>
@ -24,25 +24,29 @@ ${h.csrf_token(request)}
<div id="new-receiving-types" style="display: none;"> <div id="new-receiving-types" style="display: none;">
${h.hidden('workflow')} ${h.hidden('workflow')}
${h.hidden('phase', value='1')}
% if master.allow_from_po: % if master.allow_from_po:
## ${h.submit('submit', "Find purchase orders")} <button type="button" class="start-receiving" data-workflow="from_po">Receive from PO</button>
<button type="button">Receive from PO</button>
% endif % endif
% if master.allow_from_scratch: % if master.allow_from_scratch:
<button type="button" id="receive-from-scratch">Receive from Scratch</button> <button type="button" class="start-receiving" data-workflow="from_scratch">Receive from Scratch</button>
% endif % endif
% if master.allow_truck_dump: % if master.allow_truck_dump:
<button type="button" id="receive-truck-dump">Receive Truck Dump</button> <button type="button" class="start-receiving" data-workflow="truck_dump">Receive Truck Dump</button>
% endif % endif
</div> </div>
% else: ## vendor is known % else: ## phase 2
${h.hidden('workflow')}
${h.hidden('phase', value='2')}
<div class="field-wrapper vendor"> <div class="field-wrapper vendor">
<label>Vendor</label>
<div class="field"> <div class="field">
${h.hidden('vendor', value=vendor.uuid)} ${h.hidden('vendor', value=vendor.uuid)}
${vendor} ${vendor}
@ -50,17 +54,22 @@ ${h.csrf_token(request)}
</div> </div>
% if purchases: % if purchases:
${h.hidden('purchase')} ${h.hidden(purchase_order_fieldname, class_='purchase-order-field')}
<p>Please choose a Purchase Order to receive:</p>
<ul data-role="listview" data-inset="true"> <ul data-role="listview" data-inset="true">
% for uuid, purchase in purchases: % for key, purchase in purchases:
<li data-uuid="${uuid}">${h.link_to(purchase, '#')}</li> <li data-key="${key}">${h.link_to(purchase, '#')}</li>
% endfor % endfor
</ul> </ul>
% else: % else:
<p>(no eligible purchases found)</p> <p>(no eligible purchases found)</p>
% endif % endif
## ${h.link_to("Receive from scratch for {}".format(vendor), '#', class_='ui-btn ui-corner-all')} % if master.allow_from_scratch:
<button type="button" class="start-receiving" data-workflow="from_scratch">Receive from Scratch</button>
% endif
${h.link_to("Cancel", url('mobile.{}'.format(route_prefix)), class_='ui-btn ui-corner-all')}
% endif % endif

View file

@ -560,9 +560,11 @@ class PurchasingBatchView(BatchMasterView):
if self.batch_mode in (self.enum.PURCHASE_BATCH_MODE_RECEIVING, if self.batch_mode in (self.enum.PURCHASE_BATCH_MODE_RECEIVING,
self.enum.PURCHASE_BATCH_MODE_COSTING): self.enum.PURCHASE_BATCH_MODE_COSTING):
if batch.purchase_uuid: purchase = batch.purchase
if not purchase and batch.purchase_uuid:
purchase = self.Session.query(model.Purchase).get(batch.purchase_uuid) purchase = self.Session.query(model.Purchase).get(batch.purchase_uuid)
assert purchase assert purchase
if purchase:
kwargs['purchase'] = purchase kwargs['purchase'] = purchase
kwargs['buyer'] = purchase.buyer kwargs['buyer'] = purchase.buyer
kwargs['buyer_uuid'] = purchase.buyer_uuid kwargs['buyer_uuid'] = purchase.buyer_uuid

View file

@ -66,8 +66,19 @@ class MobileItemStatusFilter(grids.filters.MobileFilter):
# TODO: is this accurate (enough) ? # TODO: is this accurate (enough) ?
if value == 'incomplete': if value == 'incomplete':
return query.filter(sa.or_(model.PurchaseBatchRow.cases_ordered != 0, model.PurchaseBatchRow.units_ordered != 0))\ return query.filter(sa.or_(model.PurchaseBatchRow.cases_ordered != 0,
.filter(model.PurchaseBatchRow.status_code != model.PurchaseBatchRow.STATUS_OK) model.PurchaseBatchRow.units_ordered != 0))\
.filter(~model.PurchaseBatchRow.status_code.in_((
model.PurchaseBatchRow.STATUS_OK,
model.PurchaseBatchRow.STATUS_PRODUCT_NOT_FOUND)))
if value == 'invalid':
return query.filter(model.PurchaseBatchRow.status_code.in_((
model.PurchaseBatchRow.STATUS_PRODUCT_NOT_FOUND,
model.PurchaseBatchRow.STATUS_COST_NOT_FOUND,
model.PurchaseBatchRow.STATUS_CASE_QUANTITY_UNKNOWN,
model.PurchaseBatchRow.STATUS_CASE_QUANTITY_DIFFERS,
)))
if value == 'unexpected': if value == 'unexpected':
return query.filter(sa.and_( return query.filter(sa.and_(
@ -118,6 +129,8 @@ class ReceivingBatchView(PurchasingBatchView):
default_uom_is_case = True default_uom_is_case = True
purchase_order_fieldname = 'purchase'
labels = { labels = {
'truck_dump_batch': "Truck Dump Parent", 'truck_dump_batch': "Truck Dump Parent",
'invoice_parser_key': "Invoice Parser", 'invoice_parser_key': "Invoice Parser",
@ -363,18 +376,7 @@ class ReceivingBatchView(PurchasingBatchView):
def get_batch_kwargs(self, batch, mobile=False): def get_batch_kwargs(self, batch, mobile=False):
kwargs = super(ReceivingBatchView, self).get_batch_kwargs(batch, mobile=mobile) kwargs = super(ReceivingBatchView, self).get_batch_kwargs(batch, mobile=mobile)
if not mobile:
if mobile:
if 'purchase' in self.request.POST:
purchase = self.get_purchase(self.request.POST['purchase'])
if isinstance(purchase, model.Purchase):
kwargs['purchase'] = purchase
department = self.department_for_purchase(purchase)
if department:
kwargs['department'] = department
else: # not mobile
batch_type = self.request.POST['batch_type'] batch_type = self.request.POST['batch_type']
if batch_type == 'from_scratch': if batch_type == 'from_scratch':
kwargs.pop('truck_dump_batch', None) kwargs.pop('truck_dump_batch', None)
@ -516,10 +518,10 @@ class ReceivingBatchView(PurchasingBatchView):
# visible filter options will depend on whether batch came from purchase # visible filter options will depend on whether batch came from purchase
if batch.order_quantities_known: if batch.order_quantities_known:
value_choices = ['incomplete', 'unexpected', 'damaged', 'expired', 'all'] value_choices = ['incomplete', 'unexpected', 'damaged', 'expired', 'invalid', 'all']
default_status = 'incomplete' default_status = 'incomplete'
else: else:
value_choices = ['received', 'damaged', 'expired', 'all'] value_choices = ['received', 'damaged', 'expired', 'invalid', 'all']
default_status = 'all' default_status = 'all'
# remove 'expired' filter option if not relevant # remove 'expired' filter option if not relevant
@ -540,10 +542,12 @@ class ReceivingBatchView(PurchasingBatchView):
""" """
mode = self.batch_mode mode = self.batch_mode
data = {'mode': mode} data = {'mode': mode}
phase = 1
schema = MobileNewReceivingBatch().bind(session=self.Session()) schema = MobileNewReceivingBatch().bind(session=self.Session())
form = forms.Form(schema=schema, request=self.request) form = forms.Form(schema=schema, request=self.request)
if form.validate(newstyle=True): if form.validate(newstyle=True):
phase = form.validated['phase']
if form.validated['workflow'] == 'from_scratch': if form.validated['workflow'] == 'from_scratch':
if not self.allow_from_scratch: if not self.allow_from_scratch:
@ -556,7 +560,7 @@ class ReceivingBatchView(PurchasingBatchView):
batch.date_received = localtime(self.rattail_config).date() batch.date_received = localtime(self.rattail_config).date()
kwargs = self.get_batch_kwargs(batch, mobile=True) kwargs = self.get_batch_kwargs(batch, mobile=True)
batch = self.handler.make_batch(self.Session(), **kwargs) batch = self.handler.make_batch(self.Session(), **kwargs)
return self.redirect(self.request.route_url('mobile.receiving.view', uuid=batch.uuid)) return self.redirect(self.get_action_url('view', batch, mobile=True))
elif form.validated['workflow'] == 'truck_dump': elif form.validated['workflow'] == 'truck_dump':
if not self.allow_truck_dump: if not self.allow_truck_dump:
@ -565,44 +569,85 @@ class ReceivingBatchView(PurchasingBatchView):
batch.store = self.rattail_config.get_store(self.Session()) batch.store = self.rattail_config.get_store(self.Session())
batch.mode = mode batch.mode = mode
batch.truck_dump = True batch.truck_dump = True
batch.vendor = self.Session.merge(form.validated['vendor']) batch.vendor = self.Session.query(model.Vendor).get(form.validated['vendor'])
batch.created_by = self.request.user batch.created_by = self.request.user
batch.date_received = localtime(self.rattail_config).date() batch.date_received = localtime(self.rattail_config).date()
kwargs = self.get_batch_kwargs(batch, mobile=True) kwargs = self.get_batch_kwargs(batch, mobile=True)
batch = self.handler.make_batch(self.Session(), **kwargs) batch = self.handler.make_batch(self.Session(), **kwargs)
return self.redirect(self.request.route_url('mobile.receiving.view', uuid=batch.uuid)) return self.redirect(self.get_action_url('view', batch, mobile=True))
else: elif form.validated['workflow'] == 'from_po':
raise NotImplementedError("Requested workflow not supported: {}".format(form.validated['workflow'])) if not self.allow_from_po:
raise NotImplementedError("Requested workflow not supported: from_po")
vendor = None vendor = self.Session.query(model.Vendor).get(form.validated['vendor'])
if self.request.method == 'POST' and self.request.POST.get('vendor'):
vendor = self.Session.query(model.Vendor).get(self.request.POST['vendor'])
if vendor:
data['vendor'] = vendor data['vendor'] = vendor
if self.request.POST.get('purchase'): schema = self.make_mobile_receiving_from_po_schema()
purchase = self.get_purchase(self.request.POST['purchase']) po_form = forms.Form(schema=schema, request=self.request)
if purchase: if phase == 2:
if po_form.validate(newstyle=True):
batch = self.model_class() batch = self.model_class()
batch.store = self.rattail_config.get_store(self.Session())
batch.mode = mode batch.mode = mode
batch.vendor = vendor batch.vendor = vendor
batch.store = self.rattail_config.get_store(self.Session())
batch.buyer = self.request.user.employee batch.buyer = self.request.user.employee
batch.created_by = self.request.user batch.created_by = self.request.user
batch.date_received = localtime(self.rattail_config).date()
self.assign_purchase_order(batch, po_form)
kwargs = self.get_batch_kwargs(batch, mobile=True) kwargs = self.get_batch_kwargs(batch, mobile=True)
batch = self.handler.make_batch(self.Session(), **kwargs) batch = self.handler.make_batch(self.Session(), **kwargs)
if self.handler.should_populate(batch): if self.handler.should_populate(batch):
self.handler.populate(batch) self.handler.populate(batch)
return self.redirect(self.request.route_url('mobile.receiving.view', uuid=batch.uuid)) return self.redirect(self.get_action_url('view', batch, mobile=True))
else:
phase = 2
else:
raise NotImplementedError("Requested workflow not supported: {}".format(form.validated['workflow']))
data['form'] = form
data['dform'] = form.make_deform_form()
data['mode_title'] = self.enum.PURCHASE_BATCH_MODE[mode].capitalize() data['mode_title'] = self.enum.PURCHASE_BATCH_MODE[mode].capitalize()
if vendor: data['phase'] = phase
if phase == 2:
purchases = self.eligible_purchases(vendor.uuid, mode=mode) purchases = self.eligible_purchases(vendor.uuid, mode=mode)
data['purchases'] = [(p['key'], p['display']) for p in purchases['purchases']] data['purchases'] = [(p['key'], p['display']) for p in purchases['purchases']]
data['purchase_order_fieldname'] = self.purchase_order_fieldname
return self.render_to_response('create', data, mobile=True) return self.render_to_response('create', data, mobile=True)
def make_mobile_receiving_from_po_schema(self):
schema = colander.MappingSchema()
schema.add(colander.SchemaNode(colander.String(),
name=self.purchase_order_fieldname,
validator=self.validate_purchase))
return schema.bind(session=self.Session())
@staticmethod
@colander.deferred
def validate_purchase(node, kw):
session = kw['session']
def validate(node, value):
purchase = session.query(model.Purchase).get(value)
if not purchase:
raise colander.Invalid(node, "Purchase not found")
return purchase.uuid
return validate
def assign_purchase_order(self, batch, po_form):
"""
Assign the original purchase order to the given batch. Default
behavior assumes a Rattail Purchase object is what we're after.
"""
purchase = self.get_purchase(po_form.validated[self.purchase_order_fieldname])
if isinstance(purchase, model.Purchase):
batch.purchase = purchase
department = self.department_for_purchase(purchase)
if department:
batch.department = department
def configure_mobile_form(self, f): def configure_mobile_form(self, f):
super(ReceivingBatchView, self).configure_mobile_form(f) super(ReceivingBatchView, self).configure_mobile_form(f)
batch = f.model_instance batch = f.model_instance
@ -950,6 +995,13 @@ class MobileNewReceivingBatch(colander.MappingSchema):
'truck_dump', 'truck_dump',
])) ]))
phase = colander.SchemaNode(colander.Int())
class MobileNewReceivingFromPO(colander.MappingSchema):
purchase = colander.SchemaNode(colander.String())
# TODO: this is a stopgap measure to fix an obvious bug, which exists when the # TODO: this is a stopgap measure to fix an obvious bug, which exists when the
# session is not provided by the view at runtime (i.e. when it was instead # session is not provided by the view at runtime (i.e. when it was instead