From a2b7f882bceecb56636bea453fdce05947ab7848 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 30 Jan 2021 19:54:38 -0600 Subject: [PATCH] Split "new receiving batch" process into 2 steps: choose, create so that the form used to create the batch can be made custom per-workflow, and it won't have to think about any other workflows since we just use one form at a time for that --- tailbone/views/purchasing/receiving.py | 193 ++++++++++++++++++++++--- 1 file changed, 172 insertions(+), 21 deletions(-) diff --git a/tailbone/views/purchasing/receiving.py b/tailbone/views/purchasing/receiving.py index 99f84409..04d0d624 100644 --- a/tailbone/views/purchasing/receiving.py +++ b/tailbone/views/purchasing/receiving.py @@ -93,7 +93,8 @@ class ReceivingBatchView(PurchasingBatchView): form_fields = [ 'id', - 'batch_type', + 'batch_type', # TODO: ideally would get rid of this one + 'receiving_workflow', 'store', 'vendor', 'description', @@ -202,6 +203,79 @@ class ReceivingBatchView(PurchasingBatchView): def batch_mode(self): return self.enum.PURCHASE_BATCH_MODE_RECEIVING + def create(self, form=None, **kwargs): + """ + Custom view for creating a new receiving batch. We split the process + into two steps, 1) choose and 2) create. This is because the specific + form details for creating a batch will depend on which "type" of batch + creation is to be done, and it's much easier to keep conditional logic + for that in the server instead of client-side etc. + """ + route_prefix = self.get_route_prefix() + workflows = self.handler.supported_receiving_workflows() + valid_workflows = [workflow['workflow_key'] + for workflow in workflows] + + # if user has already identified their desired workflow, then we can + # just farm out to the default logic. we will of course configure our + # form differently, based on workflow, but this create() method at + # least will not need customization for that. + if 'workflow_key' in self.request.matchdict: + + # however we do have one more thing to check - the workflow + # requested must of course be valid! + workflow_key = self.request.matchdict['workflow_key'] + if workflow_key not in valid_workflows: + self.request.session.flash( + "Not a supported workflow: {}".format(workflow_key), + 'error') + raise self.redirect(self.request.route_url('{}.create'.format(route_prefix))) + + # okay now do the normal thing, per workflow + return super(ReceivingBatchView, self).create(**kwargs) + + # on the other hand, if caller provided a form, that means we are in + # the middle of some other custom workflow, e.g. "add child to truck + # dump parent" or some such. in which case we also defer to the normal + # logic, so as to not interfere with that. + if form: + return super(ReceivingBatchView, self).create(form=form, **kwargs) + + # okay, at this point we need the user to select a workflow... + self.creating = True + use_buefy = self.get_use_buefy() + context = {} + + # form to accept user choice of workflow + schema = NewBatchType().bind(valid_workflows=valid_workflows) + form = forms.Form(schema=schema, request=self.request, + use_buefy=use_buefy) + values = [(workflow['workflow_key'], workflow['display']) + for workflow in workflows] + if use_buefy: + # if workflows: + # form.set_default('workflow', workflows[0]['workflow_key']) + form.set_widget('workflow', + dfwidget.SelectWidget(values=values)) + else: + form.set_widget('workflow', + forms.widgets.JQuerySelectWidget(values=values)) + form.submit_label = "Continue" + form.cancel_url = self.get_index_url() + + # if form validates, that means user has chosen a creation type, so we + # just redirect to the appropriate "new batch of type X" page + if form.validate(newstyle=True): + workflow_key = form.validated['workflow'] + url = self.request.route_url('{}.create_type'.format(route_prefix), + workflow_key=workflow_key) + raise self.redirect(url) + + context['form'] = form + if hasattr(form, 'make_deform_form'): + context['dform'] = form.make_deform_form() + return self.render_to_response('create', context) + def row_deletable(self, row): batch = row.batch @@ -243,18 +317,25 @@ class ReceivingBatchView(PurchasingBatchView): super(ReceivingBatchView, self).configure_form(f) batch = f.model_instance allow_truck_dump = self.handler.allow_truck_dump_receiving() + workflow = self.request.matchdict.get('workflow_key') + route_prefix = self.get_route_prefix() + + # cancel should take us back to choosing a workflow, when creating + if self.creating and workflow: + f.cancel_url = self.request.route_url('{}.create'.format(route_prefix)) + + # receiving_workflow + if self.creating and workflow: + f.set_readonly('receiving_workflow') + f.set_renderer('receiving_workflow', self.render_receiving_workflow) + else: + f.remove('receiving_workflow') # batch_type if self.creating: - batch_types = OrderedDict() - if self.handler.allow_receiving_from_scratch(): - batch_types['from_scratch'] = "From Scratch" - if self.handler.allow_receiving_from_purchase_order(): - batch_types['from_po'] = "From PO" - if allow_truck_dump: - batch_types['truck_dump_children_first'] = "Truck Dump (children FIRST)" - batch_types['truck_dump_children_last'] = "Truck Dump (children LAST)" - f.set_enum('batch_type', batch_types) + f.set_widget('batch_type', dfwidget.HiddenWidget()) + f.set_default('batch_type', workflow) + f.set_hidden('batch_type') else: f.remove_field('batch_type') @@ -361,6 +442,44 @@ class ReceivingBatchView(PurchasingBatchView): # invoice totals f.set_label('invoice_total', "Invoice Total (Orig.)") f.set_label('invoice_total_calculated', "Invoice Total (Calc.)") + if self.creating: + f.remove('invoice_total_calculated') + + # receiving_complete + if self.creating: + f.remove('receiving_complete') + + # now that all fields are setup, we get rid of some, based on workflow + if self.creating: + + if workflow == 'from_scratch': + f.remove('truck_dump_batch_uuid', + 'invoice_file', + 'invoice_parser_key') + + elif workflow == 'truck_dump_children_first': + f.remove('truck_dump_batch_uuid', + 'invoice_file', + 'invoice_parser_key', + 'date_ordered', + 'po_number', + 'invoice_date', + 'invoice_number') + + elif workflow == 'truck_dump_children_last': + f.remove('truck_dump_batch_uuid', + 'invoice_file', + 'invoice_parser_key', + 'date_ordered', + 'po_number', + 'invoice_date', + 'invoice_number') + + def render_receiving_workflow(self, batch, field): + key = self.request.matchdict['workflow_key'] + info = self.handler.receiving_workflow_info(key) + if info: + return info['display'] def template_kwargs_create(self, **kwargs): kwargs = super(ReceivingBatchView, self).template_kwargs_create(**kwargs) @@ -488,6 +607,9 @@ class ReceivingBatchView(PurchasingBatchView): self.configure_form(f) + # cancel should go back to truck dump parent + f.cancel_url = self.get_action_url('view', truck_dump) + f.set_fields([ 'batch_type', 'truck_dump_parent', @@ -502,6 +624,7 @@ class ReceivingBatchView(PurchasingBatchView): # batch_type f.set_widget('batch_type', forms.widgets.ReadonlyWidget()) f.set_default('batch_type', 'truck_dump_child_from_invoice') + f.set_hidden('batch_type', False) # truck_dump_batch_uuid f.set_readonly('truck_dump_parent') @@ -1272,6 +1395,13 @@ class ReceivingBatchView(PurchasingBatchView): progress.session['success_url'] = success_url progress.session.save() + @classmethod + def defaults(cls, config): + cls._receiving_defaults(config) + cls._purchasing_defaults(config) + cls._batch_defaults(config) + cls._defaults(config) + @classmethod def _receiving_defaults(cls, config): rattail_config = config.registry.settings.get('rattail_config') @@ -1281,13 +1411,18 @@ class ReceivingBatchView(PurchasingBatchView): model_key = cls.get_model_key() permission_prefix = cls.get_permission_prefix() + # new receiving batch using workflow X + config.add_route('{}.create_type'.format(route_prefix), '{}/new/{{workflow_key}}'.format(url_prefix)) + config.add_view(cls, attr='create', route_name='{}.create_type'.format(route_prefix), + permission='{}.create'.format(permission_prefix)) + # row-level receiving - config.add_route('{}.receive_row'.format(route_prefix), '{}/{{uuid}}/rows/{{row_uuid}}/receive'.format(url_prefix)) + config.add_route('{}.receive_row'.format(route_prefix), '{}/rows/{{row_uuid}}/receive'.format(instance_url_prefix)) config.add_view(cls, attr='receive_row', route_name='{}.receive_row'.format(route_prefix), permission='{}.edit_row'.format(permission_prefix)) # declare credit for row - config.add_route('{}.declare_credit'.format(route_prefix), '{}/{{uuid}}/rows/{{row_uuid}}/declare-credit'.format(url_prefix)) + config.add_route('{}.declare_credit'.format(route_prefix), '{}/rows/{{row_uuid}}/declare-credit'.format(instance_url_prefix)) config.add_view(cls, attr='declare_credit', route_name='{}.declare_credit'.format(route_prefix), permission='{}.edit_row'.format(permission_prefix)) @@ -1298,29 +1433,45 @@ class ReceivingBatchView(PurchasingBatchView): renderer='json') # add TD child batch, from invoice file - config.add_route('{}.add_child_from_invoice'.format(route_prefix), '{}/{{{}}}/add-child-from-invoice'.format(url_prefix, model_key)) + config.add_route('{}.add_child_from_invoice'.format(route_prefix), '{}/add-child-from-invoice'.format(instance_url_prefix)) config.add_view(cls, attr='add_child_from_invoice', route_name='{}.add_child_from_invoice'.format(route_prefix), permission='{}.create'.format(permission_prefix)) # transform TD parent row from "pack" to "unit" item - config.add_route('{}.transform_unit_row'.format(route_prefix), '{}/{{{}}}/transform-unit'.format(url_prefix, model_key)) + config.add_route('{}.transform_unit_row'.format(route_prefix), '{}/transform-unit'.format(instance_url_prefix)) config.add_view(cls, attr='transform_unit_row', route_name='{}.transform_unit_row'.format(route_prefix), permission='{}.edit_row'.format(permission_prefix), renderer='json') # auto-receive all items if not rattail_config.production(): - config.add_route('{}.auto_receive'.format(route_prefix), '{}/{{{}}}/auto-receive'.format(url_prefix, model_key), + config.add_route('{}.auto_receive'.format(route_prefix), '{}/auto-receive'.format(instance_url_prefix), request_method='POST') config.add_view(cls, attr='auto_receive', route_name='{}.auto_receive'.format(route_prefix), permission='admin') - @classmethod - def defaults(cls, config): - cls._receiving_defaults(config) - cls._purchasing_defaults(config) - cls._batch_defaults(config) - cls._defaults(config) +@colander.deferred +def valid_workflow(node, kw): + """ + Deferred validator for ``workflow`` field, for new batches. + """ + valid_workflows = kw['valid_workflows'] + + def validate(node, value): + # we just need to provide possible values, and let stock validator + # handle the rest + oneof = colander.OneOf(valid_workflows) + return oneof(node, value) + + return validate + + +class NewBatchType(colander.Schema): + """ + Schema for choosing which "type" of new receiving batch should be created. + """ + workflow = colander.SchemaNode(colander.String(), + validator=valid_workflow) class ReceiveRowForm(colander.MappingSchema):