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
This commit is contained in:
Lance Edgar 2021-01-30 19:54:38 -06:00
parent ff2e39f67a
commit a2b7f882bc

View file

@ -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):