Various changes to support current receiving workflows
i.e. for sake of truck dump, adding child from invoice etc.
This commit is contained in:
parent
210508480e
commit
b0e8f7d985
|
@ -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!
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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([
|
||||||
|
|
Loading…
Reference in a new issue