Various changes to support current receiving workflows

i.e. for sake of truck dump, adding child from invoice etc.
This commit is contained in:
Lance Edgar 2018-05-22 13:54:50 -05:00
parent 210508480e
commit b0e8f7d985
7 changed files with 153 additions and 44 deletions

View file

@ -605,6 +605,9 @@ class Form(object):
else: else:
self.enums.pop(key, None) self.enums.pop(key, None)
def get_enum(self, key):
return self.enums.get(key)
def set_renderer(self, key, renderer): def set_renderer(self, key, renderer):
if renderer is None: if renderer is None:
if key in self.renderers: if key in self.renderers:
@ -810,6 +813,10 @@ class Form(object):
except TypeError: except TypeError:
return getattr(record, field_name, None) return getattr(record, field_name, None)
# TODO: is this always safe to do?
elif self.defaults and field_name in self.defaults:
return self.defaults[field_name]
def validate(self, *args, **kwargs): def validate(self, *args, **kwargs):
if kwargs.pop('newstyle', False): if kwargs.pop('newstyle', False):
# yay, new behavior! # yay, new behavior!

View file

@ -25,7 +25,7 @@
% if master.editable and request.has_perm('{}.edit'.format(permission_prefix)): % if master.editable and request.has_perm('{}.edit'.format(permission_prefix)):
<li>${h.link_to("Edit this {}".format(model_title), action_url('edit', instance))}</li> <li>${h.link_to("Edit this {}".format(model_title), action_url('edit', instance))}</li>
% endif % endif
% if master.creatable and request.has_perm('{}.create'.format(permission_prefix)): % if master.creatable and master.show_create_link and request.has_perm('{}.create'.format(permission_prefix)):
<li>${h.link_to("Create a new {}".format(model_title), url('{}.create'.format(route_prefix)))}</li> <li>${h.link_to("Create a new {}".format(model_title), url('{}.create'.format(route_prefix)))}</li>
% endif % endif
</%def> </%def>

View file

@ -27,7 +27,7 @@
% if master.deletable and instance_deletable and request.has_perm('{}.delete'.format(permission_prefix)): % if master.deletable and instance_deletable and request.has_perm('{}.delete'.format(permission_prefix)):
<li>${h.link_to("Delete this {}".format(model_title), action_url('delete', instance))}</li> <li>${h.link_to("Delete this {}".format(model_title), action_url('delete', instance))}</li>
% endif % endif
% if master.creatable and request.has_perm('{}.create'.format(permission_prefix)): % if master.creatable and master.show_create_link and request.has_perm('{}.create'.format(permission_prefix)):
<li>${h.link_to("Create a new {}".format(model_title), url('{}.create'.format(route_prefix)))}</li> <li>${h.link_to("Create a new {}".format(model_title), url('{}.create'.format(route_prefix)))}</li>
% endif % endif
</%def> </%def>

View file

@ -67,7 +67,7 @@
% if master.results_downloadable_xlsx and request.has_perm('{}.results_xlsx'.format(permission_prefix)): % if master.results_downloadable_xlsx and request.has_perm('{}.results_xlsx'.format(permission_prefix)):
<li>${h.link_to("Download results as XLSX", url('{}.results_xlsx'.format(route_prefix)))}</li> <li>${h.link_to("Download results as XLSX", url('{}.results_xlsx'.format(route_prefix)))}</li>
% endif % endif
% if master.creatable and request.has_perm('{}.create'.format(permission_prefix)): % if master.creatable and master.show_create_link and request.has_perm('{}.create'.format(permission_prefix)):
% if master.creates_multiple: % if master.creates_multiple:
<li>${h.link_to("Create new {}".format(model_title_plural), url('{}.create'.format(route_prefix)))}</li> <li>${h.link_to("Create new {}".format(model_title_plural), url('{}.create'.format(route_prefix)))}</li>
% else: % else:

View file

@ -54,7 +54,7 @@
% if master.deletable and instance_deletable and request.has_perm('{}.delete'.format(permission_prefix)): % if master.deletable and instance_deletable and request.has_perm('{}.delete'.format(permission_prefix)):
<li>${h.link_to("Delete this {}".format(model_title), action_url('delete', instance), class_='delete-instance')}</li> <li>${h.link_to("Delete this {}".format(model_title), action_url('delete', instance), class_='delete-instance')}</li>
% endif % endif
% if master.creatable and request.has_perm('{}.create'.format(permission_prefix)): % if master.creatable and master.show_create_link and request.has_perm('{}.create'.format(permission_prefix)):
% if master.creates_multiple: % if master.creates_multiple:
<li>${h.link_to("Create new {}".format(model_title_plural), url('{}.create'.format(route_prefix)))}</li> <li>${h.link_to("Create new {}".format(model_title_plural), url('{}.create'.format(route_prefix)))}</li>
% else: % else:

View file

@ -72,6 +72,7 @@ class MasterView(View):
results_downloadable_csv = False results_downloadable_csv = False
results_downloadable_xlsx = False results_downloadable_xlsx = False
creatable = True creatable = True
show_create_link = True
viewable = True viewable = True
editable = True editable = True
deletable = True deletable = True
@ -601,12 +602,13 @@ class MasterView(View):
def render_mobile_row_listitem(self, obj, i): def render_mobile_row_listitem(self, obj, i):
return obj return obj
def create(self): def create(self, form=None):
""" """
View for creating a new model record. View for creating a new model record.
""" """
self.creating = True self.creating = True
form = self.make_form(self.get_model_class()) if form is None:
form = self.make_form(self.get_model_class())
if self.request.method == 'POST': if self.request.method == 'POST':
if self.validate_form(form): if self.validate_form(form):
# let save_create_form() return alternate object if necessary # let save_create_form() return alternate object if necessary
@ -2200,7 +2202,7 @@ class MasterView(View):
except os.error: except os.error:
return 0 return 0
def make_form(self, instance=None, factory=None, fields=None, schema=None, **kwargs): def make_form(self, instance=None, factory=None, fields=None, schema=None, make_kwargs=None, configure=None, **kwargs):
""" """
Creates a new form for the given model class/instance Creates a new form for the given model class/instance
""" """
@ -2210,15 +2212,19 @@ class MasterView(View):
fields = self.get_form_fields() fields = self.get_form_fields()
if schema is None: if schema is None:
schema = self.make_form_schema() schema = self.make_form_schema()
if make_kwargs is None:
make_kwargs = self.make_form_kwargs
if configure is None:
configure = self.configure_form
# TODO: SQLAlchemy class instance is assumed *unless* we get a dict # TODO: SQLAlchemy class instance is assumed *unless* we get a dict
# (seems like we should be smarter about this somehow) # (seems like we should be smarter about this somehow)
# if not self.creating and not isinstance(instance, dict): # if not self.creating and not isinstance(instance, dict):
if not self.creating: if not self.creating:
kwargs['model_instance'] = instance kwargs['model_instance'] = instance
kwargs = self.make_form_kwargs(**kwargs) kwargs = make_kwargs(**kwargs)
form = factory(fields, schema, **kwargs) form = factory(fields, schema, **kwargs)
self.configure_form(form) configure(form)
return form return form
def get_form_fields(self): def get_form_fields(self):
@ -2271,12 +2277,10 @@ class MasterView(View):
self.set_labels(form) self.set_labels(form)
def validate_form(self, form): def validate_form(self, form):
controls = self.request.POST.items() if form.validate(newstyle=True):
try: self.form_deserialized = form.validated
self.form_deserialized = form.validate(controls) return True
except deform.ValidationFailure: return False
return False
return True
def objectify(self, form, data): def objectify(self, form, data):
obj = form.schema.objectify(data, context=form.model_instance) obj = form.schema.objectify(data, context=form.model_instance)

View file

@ -35,7 +35,7 @@ from rattail import pod
from rattail.db import model, api from rattail.db import model, api
from rattail.gpc import GPC from rattail.gpc import GPC
from rattail.time import localtime from rattail.time import localtime
from rattail.util import pretty_quantity, prettify from rattail.util import pretty_quantity, prettify, OrderedDict
from rattail.vendors.invoices import iter_invoice_parsers, require_invoice_parser from rattail.vendors.invoices import iter_invoice_parsers, require_invoice_parser
import colander import colander
@ -131,6 +131,7 @@ class ReceivingBatchView(PurchasingBatchView):
form_fields = [ form_fields = [
'id', 'id',
'batch_type', 'batch_type',
'description',
'store', 'store',
'vendor', 'vendor',
'truck_dump', 'truck_dump',
@ -234,12 +235,9 @@ class ReceivingBatchView(PurchasingBatchView):
# batch_type # batch_type
if self.creating: if self.creating:
batch_type_values = [ f.set_enum('batch_type', OrderedDict([
('from_scratch', "New from Scratch"), ('from_scratch', "New from Scratch"),
] ]))
if self.allow_truck_dump:
batch_type_values.append(('truck_dump', "Invoice for Truck Dump"))
f.set_widget('batch_type', forms.widgets.JQuerySelectWidget(values=batch_type_values))
else: else:
f.remove_field('batch_type') f.remove_field('batch_type')
@ -285,6 +283,12 @@ class ReceivingBatchView(PurchasingBatchView):
else: else:
f.remove_field('truck_dump_batch') f.remove_field('truck_dump_batch')
# truck_dump_vendor
if self.creating:
f.set_label('truck_dump_vendor', "Vendor")
f.set_readonly('truck_dump_vendor')
f.set_renderer('truck_dump_vendor', self.render_truck_dump_vendor)
else: else:
f.remove_fields('truck_dump', f.remove_fields('truck_dump',
'truck_dump_children', 'truck_dump_children',
@ -341,8 +345,11 @@ class ReceivingBatchView(PurchasingBatchView):
if batch_type == 'from_scratch': if batch_type == 'from_scratch':
kwargs.pop('truck_dump_batch', None) kwargs.pop('truck_dump_batch', None)
kwargs.pop('truck_dump_batch_uuid', None) kwargs.pop('truck_dump_batch_uuid', None)
elif batch_type == 'truck_dump': elif batch_type.startswith('truck_dump_child'):
pass truck_dump = self.get_instance()
kwargs['store'] = truck_dump.store
kwargs['vendor'] = truck_dump.vendor
kwargs['truck_dump_batch'] = truck_dump
else: else:
raise NotImplementedError raise NotImplementedError
return kwargs return kwargs
@ -367,16 +374,91 @@ class ReceivingBatchView(PurchasingBatchView):
url = self.request.route_url('receiving.view', uuid=truck_dump.uuid) url = self.request.route_url('receiving.view', uuid=truck_dump.uuid)
return tags.link_to(text, url) return tags.link_to(text, url)
def render_truck_dump_vendor(self, batch, field):
truck_dump = self.get_instance()
vendor = truck_dump.vendor
text = "({}) {}".format(vendor.id, vendor.name)
url = self.request.route_url('vendors.view', uuid=vendor.uuid)
return tags.link_to(text, url)
def render_truck_dump_children(self, batch, field): def render_truck_dump_children(self, batch, field):
contents = []
children = batch.truck_dump_children children = batch.truck_dump_children
if not children: if children:
items = []
for child in children:
text = six.text_type(child)
url = self.request.route_url('receiving.view', uuid=child.uuid)
items.append(HTML.tag('li', c=[tags.link_to(text, url)]))
contents.append(HTML.tag('ul', c=items))
if batch.complete and not batch.executed:
buttons = self.make_truck_dump_child_buttons(batch)
if buttons:
buttons = HTML.literal(' ').join(buttons)
contents.append(HTML.tag('div', class_='buttons', c=[buttons]))
if not contents:
return "" return ""
items = [] return HTML.tag('div', c=contents)
for child in children:
text = six.text_type(child) def make_truck_dump_child_buttons(self, batch):
url = self.request.route_url('receiving.view', uuid=child.uuid) return [
items.append(HTML.tag('li', c=[tags.link_to(text, url)])) tags.link_to("Add from Invoice File", self.get_action_url('add_child_from_invoice', batch), class_='button autodisable'),
return HTML.tag('ul', c=items) ]
def add_child_from_invoice(self):
"""
View for adding a child batch to a truck dump, from invoice file.
"""
batch = self.get_instance()
if not batch.truck_dump:
self.request.session.flash("Batch is not a truck dump: {}".format(batch))
return self.redirect(self.get_action_url('view', batch))
if batch.executed:
self.request.session.flash("Batch has already been executed: {}".format(batch))
return self.redirect(self.get_action_url('view', batch))
if not batch.complete:
self.request.session.flash("Batch is not marked as complete: {}".format(batch))
return self.redirect(self.get_action_url('view', batch))
self.creating = True
form = self.make_child_from_invoice_form(self.get_model_class())
return self.create(form=form)
def make_child_from_invoice_form(self, instance, **kwargs):
"""
Creates a new form for the given model class/instance
"""
kwargs['configure'] = self.configure_child_from_invoice_form
return self.make_form(instance=instance, **kwargs)
def configure_child_from_invoice_form(self, f):
assert self.creating
truck_dump = self.get_instance()
self.configure_form(f)
f.set_fields([
'batch_type',
'truck_dump_parent',
'truck_dump_vendor',
'invoice_file',
'invoice_parser_key',
'description',
'notes',
])
# batch_type
f.set_widget('batch_type', forms.widgets.ReadonlyWidget())
f.set_default('batch_type', 'truck_dump_child_from_invoice')
# truck_dump_batch_uuid
f.set_readonly('truck_dump_parent')
f.set_renderer('truck_dump_parent', self.render_truck_dump_parent)
def render_truck_dump_parent(self, batch, field):
truck_dump = self.get_instance()
text = six.text_type(truck_dump)
url = self.request.route_url('receiving.view', uuid=truck_dump.uuid)
return tags.link_to(text, url)
def render_mobile_listitem(self, batch, i): def render_mobile_listitem(self, batch, i):
title = "({}) {} for ${:0,.2f} - {}, {}".format( title = "({}) {} for ${:0,.2f} - {}, {}".format(
@ -565,9 +647,10 @@ class ReceivingBatchView(PurchasingBatchView):
} }
if self.request.has_perm('{}.create_row'.format(permission_prefix)): if self.request.has_perm('{}.create_row'.format(permission_prefix)):
update_form = forms.Form(schema=MobileReceivingForm(), request=self.request) schema = MobileReceivingForm().bind(session=self.Session())
update_form = forms.Form(schema=schema, request=self.request)
if update_form.validate(newstyle=True): if update_form.validate(newstyle=True):
row = self.Session.merge(update_form.validated['row']) row = self.Session.query(model.PurchaseBatchRow).get(update_form.validated['row'])
# TODO: surely this (delete_row) should be split out to a separate view # TODO: surely this (delete_row) should be split out to a separate view
if update_form.validated['delete_row']: if update_form.validated['delete_row']:
@ -646,7 +729,7 @@ class ReceivingBatchView(PurchasingBatchView):
return credit return credit
@classmethod @classmethod
def defaults(cls, config): def _receiving_defaults(cls, config):
route_prefix = cls.get_route_prefix() route_prefix = cls.get_route_prefix()
url_prefix = cls.get_url_prefix() url_prefix = cls.get_url_prefix()
model_key = cls.get_model_key() model_key = cls.get_model_key()
@ -657,21 +740,19 @@ class ReceivingBatchView(PurchasingBatchView):
config.add_view(cls, attr='mobile_lookup', route_name='mobile.{}.lookup'.format(route_prefix), config.add_view(cls, attr='mobile_lookup', route_name='mobile.{}.lookup'.format(route_prefix),
renderer='json', permission='{}.create_row'.format(permission_prefix)) renderer='json', permission='{}.create_row'.format(permission_prefix))
if cls.allow_truck_dump:
config.add_route('{}.add_child_from_invoice'.format(route_prefix), '{}/{{{}}}/add-child-from-invoice'.format(url_prefix, model_key))
config.add_view(cls, attr='add_child_from_invoice', route_name='{}.add_child_from_invoice'.format(route_prefix),
permission='{}.create'.format(permission_prefix))
@classmethod
def defaults(cls, config):
cls._receiving_defaults(config)
cls._purchasing_defaults(config) cls._purchasing_defaults(config)
cls._batch_defaults(config) cls._batch_defaults(config)
cls._defaults(config) cls._defaults(config)
class PurchaseBatchRowType(forms.types.ObjectType):
model_class = model.PurchaseBatchRow
def deserialize(self, node, cstruct):
row = super(PurchaseBatchRowType, self).deserialize(node, cstruct)
if row and row.batch.executed:
raise colander.Invalid(node, "Batch has already been executed")
return row
class MobileNewReceivingBatch(colander.MappingSchema): class MobileNewReceivingBatch(colander.MappingSchema):
vendor = colander.SchemaNode(forms.types.VendorType()) vendor = colander.SchemaNode(forms.types.VendorType())
@ -684,9 +765,26 @@ class MobileNewReceivingBatch(colander.MappingSchema):
])) ]))
# 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
# being provided by the type instance, which was created upon app startup).
@colander.deferred
def valid_purchase_batch_row(node, kw):
session = kw['session']
def validate(node, value):
row = session.query(model.PurchaseBatchRow).get(value)
if not row:
raise colander.Invalid(node, "Batch row not found")
if row.batch.executed:
raise colander.Invalid(node, "Batch has already been executed")
return row.uuid
return validate
class MobileReceivingForm(colander.MappingSchema): class MobileReceivingForm(colander.MappingSchema):
row = colander.SchemaNode(PurchaseBatchRowType()) row = colander.SchemaNode(colander.String(),
validator=valid_purchase_batch_row)
mode = colander.SchemaNode(colander.String(), mode = colander.SchemaNode(colander.String(),
validator=colander.OneOf([ validator=colander.OneOf([