diff --git a/tailbone/forms/core.py b/tailbone/forms/core.py
index 4ffc73d3..8401729a 100644
--- a/tailbone/forms/core.py
+++ b/tailbone/forms/core.py
@@ -605,6 +605,9 @@ class Form(object):
else:
self.enums.pop(key, None)
+ def get_enum(self, key):
+ return self.enums.get(key)
+
def set_renderer(self, key, renderer):
if renderer is None:
if key in self.renderers:
@@ -810,6 +813,10 @@ class Form(object):
except TypeError:
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):
if kwargs.pop('newstyle', False):
# yay, new behavior!
diff --git a/tailbone/templates/master/delete.mako b/tailbone/templates/master/delete.mako
index 85892e35..da247e5c 100644
--- a/tailbone/templates/master/delete.mako
+++ b/tailbone/templates/master/delete.mako
@@ -25,7 +25,7 @@
% if master.editable and request.has_perm('{}.edit'.format(permission_prefix)):
${h.link_to("Edit this {}".format(model_title), action_url('edit', instance))}
% 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)):
${h.link_to("Create a new {}".format(model_title), url('{}.create'.format(route_prefix)))}
% endif
%def>
diff --git a/tailbone/templates/master/edit.mako b/tailbone/templates/master/edit.mako
index 6d4a9a60..4a93f8a7 100644
--- a/tailbone/templates/master/edit.mako
+++ b/tailbone/templates/master/edit.mako
@@ -27,7 +27,7 @@
% if master.deletable and instance_deletable and request.has_perm('{}.delete'.format(permission_prefix)):
${h.link_to("Delete this {}".format(model_title), action_url('delete', instance))}
% 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)):
${h.link_to("Create a new {}".format(model_title), url('{}.create'.format(route_prefix)))}
% endif
%def>
diff --git a/tailbone/templates/master/index.mako b/tailbone/templates/master/index.mako
index 8a9a9d7a..56f8c4f6 100644
--- a/tailbone/templates/master/index.mako
+++ b/tailbone/templates/master/index.mako
@@ -67,7 +67,7 @@
% if master.results_downloadable_xlsx and request.has_perm('{}.results_xlsx'.format(permission_prefix)):
${h.link_to("Download results as XLSX", url('{}.results_xlsx'.format(route_prefix)))}
% 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:
${h.link_to("Create new {}".format(model_title_plural), url('{}.create'.format(route_prefix)))}
% else:
diff --git a/tailbone/templates/master/view.mako b/tailbone/templates/master/view.mako
index 7456c9a3..a584bed5 100644
--- a/tailbone/templates/master/view.mako
+++ b/tailbone/templates/master/view.mako
@@ -54,7 +54,7 @@
% if master.deletable and instance_deletable and request.has_perm('{}.delete'.format(permission_prefix)):
${h.link_to("Delete this {}".format(model_title), action_url('delete', instance), class_='delete-instance')}
% 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:
${h.link_to("Create new {}".format(model_title_plural), url('{}.create'.format(route_prefix)))}
% else:
diff --git a/tailbone/views/master.py b/tailbone/views/master.py
index 8f08c4db..e1c93f39 100644
--- a/tailbone/views/master.py
+++ b/tailbone/views/master.py
@@ -72,6 +72,7 @@ class MasterView(View):
results_downloadable_csv = False
results_downloadable_xlsx = False
creatable = True
+ show_create_link = True
viewable = True
editable = True
deletable = True
@@ -601,12 +602,13 @@ class MasterView(View):
def render_mobile_row_listitem(self, obj, i):
return obj
- def create(self):
+ def create(self, form=None):
"""
View for creating a new model record.
"""
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.validate_form(form):
# let save_create_form() return alternate object if necessary
@@ -2200,7 +2202,7 @@ class MasterView(View):
except os.error:
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
"""
@@ -2210,15 +2212,19 @@ class MasterView(View):
fields = self.get_form_fields()
if schema is None:
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
# (seems like we should be smarter about this somehow)
# if not self.creating and not isinstance(instance, dict):
if not self.creating:
kwargs['model_instance'] = instance
- kwargs = self.make_form_kwargs(**kwargs)
+ kwargs = make_kwargs(**kwargs)
form = factory(fields, schema, **kwargs)
- self.configure_form(form)
+ configure(form)
return form
def get_form_fields(self):
@@ -2271,12 +2277,10 @@ class MasterView(View):
self.set_labels(form)
def validate_form(self, form):
- controls = self.request.POST.items()
- try:
- self.form_deserialized = form.validate(controls)
- except deform.ValidationFailure:
- return False
- return True
+ if form.validate(newstyle=True):
+ self.form_deserialized = form.validated
+ return True
+ return False
def objectify(self, form, data):
obj = form.schema.objectify(data, context=form.model_instance)
diff --git a/tailbone/views/purchasing/receiving.py b/tailbone/views/purchasing/receiving.py
index 6c754df8..7350cf79 100644
--- a/tailbone/views/purchasing/receiving.py
+++ b/tailbone/views/purchasing/receiving.py
@@ -35,7 +35,7 @@ from rattail import pod
from rattail.db import model, api
from rattail.gpc import GPC
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
import colander
@@ -131,6 +131,7 @@ class ReceivingBatchView(PurchasingBatchView):
form_fields = [
'id',
'batch_type',
+ 'description',
'store',
'vendor',
'truck_dump',
@@ -234,12 +235,9 @@ class ReceivingBatchView(PurchasingBatchView):
# batch_type
if self.creating:
- batch_type_values = [
+ f.set_enum('batch_type', OrderedDict([
('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:
f.remove_field('batch_type')
@@ -285,6 +283,12 @@ class ReceivingBatchView(PurchasingBatchView):
else:
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:
f.remove_fields('truck_dump',
'truck_dump_children',
@@ -341,8 +345,11 @@ class ReceivingBatchView(PurchasingBatchView):
if batch_type == 'from_scratch':
kwargs.pop('truck_dump_batch', None)
kwargs.pop('truck_dump_batch_uuid', None)
- elif batch_type == 'truck_dump':
- pass
+ elif batch_type.startswith('truck_dump_child'):
+ truck_dump = self.get_instance()
+ kwargs['store'] = truck_dump.store
+ kwargs['vendor'] = truck_dump.vendor
+ kwargs['truck_dump_batch'] = truck_dump
else:
raise NotImplementedError
return kwargs
@@ -367,16 +374,91 @@ class ReceivingBatchView(PurchasingBatchView):
url = self.request.route_url('receiving.view', uuid=truck_dump.uuid)
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):
+ contents = []
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 ""
- 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)]))
- return HTML.tag('ul', c=items)
+ return HTML.tag('div', c=contents)
+
+ def make_truck_dump_child_buttons(self, batch):
+ return [
+ tags.link_to("Add from Invoice File", self.get_action_url('add_child_from_invoice', batch), class_='button autodisable'),
+ ]
+
+ 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):
title = "({}) {} for ${:0,.2f} - {}, {}".format(
@@ -565,9 +647,10 @@ class ReceivingBatchView(PurchasingBatchView):
}
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):
- 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
if update_form.validated['delete_row']:
@@ -646,7 +729,7 @@ class ReceivingBatchView(PurchasingBatchView):
return credit
@classmethod
- def defaults(cls, config):
+ def _receiving_defaults(cls, config):
route_prefix = cls.get_route_prefix()
url_prefix = cls.get_url_prefix()
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),
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._batch_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):
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):
- row = colander.SchemaNode(PurchaseBatchRowType())
+ row = colander.SchemaNode(colander.String(),
+ validator=valid_purchase_batch_row)
mode = colander.SchemaNode(colander.String(),
validator=colander.OneOf([