diff --git a/tailbone/static/js/tailbone.mobile.js b/tailbone/static/js/tailbone.mobile.js index 0c8ebee0..c168543a 100644 --- a/tailbone/static/js/tailbone.mobile.js +++ b/tailbone/static/js/tailbone.mobile.js @@ -57,7 +57,7 @@ function setfocus() { var queries = [ '#username', '#new-purchasing-batch-vendor-text', - // '.receiving-upc-search', + '#new-receiving-batch-vendor-text', ]; $.each(queries, function(i, 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 $(document).on('click', '#datasync-restart', function() { diff --git a/tailbone/static/js/tailbone.mobile.receiving.js b/tailbone/static/js/tailbone.mobile.receiving.js index 3075e00a..452a99f9 100644 --- a/tailbone/static/js/tailbone.mobile.receiving.js +++ b/tailbone/static/js/tailbone.mobile.receiving.js @@ -8,29 +8,31 @@ ************************************************************/ -// TODO: this is really just for receiving; should change form name? -$(document).on('autocompleteitemselected', 'form[name="new-purchasing-batch"] .vendor', function(event, uuid) { +// toggle visibility of "Receive" type buttons based on whether vendor is set +$(document).on('autocompleteitemselected', 'form[name="new-receiving-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) { +$(document).on('autocompleteitemcleared', 'form[name="new-receiving-batch"] .vendor', function(event) { $('#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'); - form.find('input[name="workflow"]').val('truck_dump'); + form.find('input[name="workflow"]').val($(this).data('workflow')); 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'); - 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(); + return false; }); diff --git a/tailbone/templates/mobile/receiving/create.mako b/tailbone/templates/mobile/receiving/create.mako index d5e49b1b..668ab69f 100644 --- a/tailbone/templates/mobile/receiving/create.mako +++ b/tailbone/templates/mobile/receiving/create.mako @@ -5,16 +5,16 @@ <%def name="page_title()">${h.link_to("Receiving", url('mobile.receiving'))} » New Batch -${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)} -% if vendor is Undefined: +% if phase == 1:
${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'})} +
@@ -24,25 +24,29 @@ ${h.csrf_token(request)} -% else: ## vendor is known +% else: ## phase 2 + + ${h.hidden('workflow')} + ${h.hidden('phase', value='2')}
+
${h.hidden('vendor', value=vendor.uuid)} ${vendor} @@ -50,17 +54,22 @@ ${h.csrf_token(request)}
% if purchases: - ${h.hidden('purchase')} + ${h.hidden(purchase_order_fieldname, class_='purchase-order-field')} +

Please choose a Purchase Order to receive:

% else:

(no eligible purchases found)

% endif - ## ${h.link_to("Receive from scratch for {}".format(vendor), '#', class_='ui-btn ui-corner-all')} + % if master.allow_from_scratch: + + % endif + + ${h.link_to("Cancel", url('mobile.{}'.format(route_prefix)), class_='ui-btn ui-corner-all')} % endif diff --git a/tailbone/views/purchasing/batch.py b/tailbone/views/purchasing/batch.py index 1c711854..8941de1c 100644 --- a/tailbone/views/purchasing/batch.py +++ b/tailbone/views/purchasing/batch.py @@ -560,9 +560,11 @@ class PurchasingBatchView(BatchMasterView): if self.batch_mode in (self.enum.PURCHASE_BATCH_MODE_RECEIVING, 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) assert purchase + if purchase: kwargs['purchase'] = purchase kwargs['buyer'] = purchase.buyer kwargs['buyer_uuid'] = purchase.buyer_uuid diff --git a/tailbone/views/purchasing/receiving.py b/tailbone/views/purchasing/receiving.py index 589e27d0..c27f3d78 100644 --- a/tailbone/views/purchasing/receiving.py +++ b/tailbone/views/purchasing/receiving.py @@ -66,8 +66,19 @@ class MobileItemStatusFilter(grids.filters.MobileFilter): # TODO: is this accurate (enough) ? if value == 'incomplete': - return query.filter(sa.or_(model.PurchaseBatchRow.cases_ordered != 0, model.PurchaseBatchRow.units_ordered != 0))\ - .filter(model.PurchaseBatchRow.status_code != model.PurchaseBatchRow.STATUS_OK) + return query.filter(sa.or_(model.PurchaseBatchRow.cases_ordered != 0, + 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': return query.filter(sa.and_( @@ -118,6 +129,8 @@ class ReceivingBatchView(PurchasingBatchView): default_uom_is_case = True + purchase_order_fieldname = 'purchase' + labels = { 'truck_dump_batch': "Truck Dump Parent", 'invoice_parser_key': "Invoice Parser", @@ -363,18 +376,7 @@ class ReceivingBatchView(PurchasingBatchView): def get_batch_kwargs(self, batch, mobile=False): kwargs = super(ReceivingBatchView, self).get_batch_kwargs(batch, mobile=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 + if not mobile: batch_type = self.request.POST['batch_type'] if batch_type == 'from_scratch': kwargs.pop('truck_dump_batch', None) @@ -516,10 +518,10 @@ class ReceivingBatchView(PurchasingBatchView): # visible filter options will depend on whether batch came from purchase if batch.order_quantities_known: - value_choices = ['incomplete', 'unexpected', 'damaged', 'expired', 'all'] + value_choices = ['incomplete', 'unexpected', 'damaged', 'expired', 'invalid', 'all'] default_status = 'incomplete' else: - value_choices = ['received', 'damaged', 'expired', 'all'] + value_choices = ['received', 'damaged', 'expired', 'invalid', 'all'] default_status = 'all' # remove 'expired' filter option if not relevant @@ -540,10 +542,12 @@ class ReceivingBatchView(PurchasingBatchView): """ mode = self.batch_mode data = {'mode': mode} + phase = 1 schema = MobileNewReceivingBatch().bind(session=self.Session()) form = forms.Form(schema=schema, request=self.request) if form.validate(newstyle=True): + phase = form.validated['phase'] if form.validated['workflow'] == 'from_scratch': if not self.allow_from_scratch: @@ -556,7 +560,7 @@ class ReceivingBatchView(PurchasingBatchView): batch.date_received = localtime(self.rattail_config).date() 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)) + return self.redirect(self.get_action_url('view', batch, mobile=True)) elif form.validated['workflow'] == '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.mode = mode 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.date_received = localtime(self.rattail_config).date() 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)) + return self.redirect(self.get_action_url('view', batch, mobile=True)) - else: - raise NotImplementedError("Requested workflow not supported: {}".format(form.validated['workflow'])) + elif form.validated['workflow'] == 'from_po': + if not self.allow_from_po: + raise NotImplementedError("Requested workflow not supported: from_po") - vendor = None - if self.request.method == 'POST' and self.request.POST.get('vendor'): - vendor = self.Session.query(model.Vendor).get(self.request.POST['vendor']) - if vendor: + vendor = self.Session.query(model.Vendor).get(form.validated['vendor']) data['vendor'] = vendor - if self.request.POST.get('purchase'): - purchase = self.get_purchase(self.request.POST['purchase']) - if purchase: - + schema = self.make_mobile_receiving_from_po_schema() + po_form = forms.Form(schema=schema, request=self.request) + if phase == 2: + if po_form.validate(newstyle=True): batch = self.model_class() + batch.store = self.rattail_config.get_store(self.Session()) batch.mode = mode batch.vendor = vendor - batch.store = self.rattail_config.get_store(self.Session()) batch.buyer = self.request.user.employee 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) batch = self.handler.make_batch(self.Session(), **kwargs) if self.handler.should_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() - if vendor: + data['phase'] = phase + if phase == 2: purchases = self.eligible_purchases(vendor.uuid, mode=mode) 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) + 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): super(ReceivingBatchView, self).configure_mobile_form(f) batch = f.model_instance @@ -950,6 +995,13 @@ class MobileNewReceivingBatch(colander.MappingSchema): '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 # session is not provided by the view at runtime (i.e. when it was instead