Add "most of" support for truck dump receiving

still not complete, but conceptually it sort of is...
This commit is contained in:
Lance Edgar 2018-05-18 15:51:47 -05:00
parent 805a1afa3f
commit cd7922f204
8 changed files with 368 additions and 76 deletions

View file

@ -42,6 +42,7 @@ import deform
from colanderalchemy import SQLAlchemySchemaNode
from colanderalchemy.schema import _creation_order
from deform import widget as dfwidget
from pyramid_deform import SessionFileUploadTempStore
from pyramid.renderers import render
from webhelpers2.html import tags, HTML
@ -585,6 +586,11 @@ class Form(object):
self.set_widget(key, dfwidget.TextAreaWidget(cols=80, rows=8))
elif type_ == 'text':
self.set_widget(key, dfwidget.TextAreaWidget(cols=80, rows=8))
elif type_ == 'file':
tmpstore = SessionFileUploadTempStore(self.request)
self.set_node(key, colander.SchemaNode(deform.FileData(),
widget=dfwidget.FileUploadWidget(tmpstore),
title=self.get_label(key)))
else:
raise ValueError("unknown type for '{}' field: {}".format(key, type_))

View file

@ -61,7 +61,13 @@
<%def name="execute_button()">
% if not batch.executed and request.has_perm('{}.execute'.format(permission_prefix)):
<button type="button" id="execute-batch"${'' if execute_enabled else ' disabled="disabled"'}>${execute_title}</button>
% if execute_enabled:
<button type="button" id="execute-batch">${execute_title}</button>
% elif why_not_execute:
<button type="button" id="execute-batch" disabled="disabled" title="${why_not_execute}">${execute_title}</button>
% else:
<button type="button" id="execute-batch" disabled="disabled">${execute_title}</button>
% endif
% endif
</%def>

View file

@ -0,0 +1,67 @@
## -*- coding: utf-8; -*-
<%inherit file="/batch/create.mako" />
<%def name="extra_javascript()">
${parent.extra_javascript()}
${self.func_show_batch_type()}
<script type="text/javascript">
% if master.allow_truck_dump:
var batch_vendor_map = ${json.dumps(batch_vendor_map)|n};
% endif
$(function() {
$('.batch_type select').on('selectmenuchange', function(event, ui) {
show_batch_type(ui.item.value);
});
$('.truck_dump_batch_uuid select').on('selectmenuchange', function(event, ui) {
var form = $(this).parents('form');
var uuid = ui.item.value ? batch_vendor_map[ui.item.value] : '';
form.find('input[name="vendor_uuid"]').val(uuid);
});
show_batch_type();
});
</script>
</%def>
<%def name="func_show_batch_type()">
<script type="text/javascript">
function show_batch_type(batch_type) {
if (batch_type === undefined) {
batch_type = $('.field-wrapper.batch_type select').val();
}
if (batch_type == 'from_scratch') {
$('.field-wrapper.truck_dump_batch_uuid').hide();
$('.field-wrapper.invoice_file').hide();
$('.field-wrapper.invoice_parser_key').hide();
$('.field-wrapper.vendor_uuid').show();
$('.field-wrapper.date_ordered').show();
$('.field-wrapper.date_received').show();
$('.field-wrapper.po_number').show();
$('.field-wrapper.invoice_date').show();
$('.field-wrapper.invoice_number').show();
} else if (batch_type == 'truck_dump') {
$('.field-wrapper.truck_dump_batch_uuid').show();
$('.field-wrapper.invoice_file').show();
$('.field-wrapper.invoice_parser_key').show();
$('.field-wrapper.vendor_uuid').hide();
$('.field-wrapper.date_ordered').hide();
$('.field-wrapper.date_received').hide();
$('.field-wrapper.po_number').hide();
$('.field-wrapper.invoice_date').hide();
$('.field-wrapper.invoice_number').hide();
}
}
</script>
</%def>
${parent.body()}

View file

@ -42,11 +42,9 @@ from rattail.util import load_object, prettify
import colander
import deform
from deform import widget as dfwidget
from pyramid import httpexceptions
from pyramid.renderers import render_to_response
from pyramid.response import FileResponse
from pyramid_deform import SessionFileUploadTempStore
from webhelpers2.html import HTML, tags
from tailbone import forms, grids
@ -131,6 +129,9 @@ class BatchMasterView(MasterView):
return load_object(spec)(self.rattail_config)
return self.batch_handler_class(self.rattail_config)
def download_path(self, batch, filename):
return self.rattail_config.batch_filepath(batch.batch_key, batch.uuid, filename)
def template_kwargs_view(self, **kwargs):
batch = kwargs['instance']
kwargs['batch'] = batch
@ -140,6 +141,8 @@ class BatchMasterView(MasterView):
if kwargs['execute_enabled']:
url = self.get_action_url('execute', batch)
kwargs['execute_form'] = self.make_execute_form(batch, action_url=url)
else:
kwargs['why_not_execute'] = self.handler.why_not_execute(batch)
return kwargs
def allow_worksheet(self, batch):
@ -278,9 +281,6 @@ class BatchMasterView(MasterView):
return status_code_text
return render_status
def download_path(self, batch, filename):
return self.rattail_config.batch_filepath(batch.batch_key, batch.uuid, filename)
def render_user(self, batch, field):
user = getattr(batch, field)
if not user:
@ -312,6 +312,7 @@ class BatchMasterView(MasterView):
f.remove_field('complete')
def save_create_form(self, form):
uploads = self.normalize_uploads(form, skip=['filename'])
self.before_create(form)
session = self.Session()
@ -346,17 +347,15 @@ class BatchMasterView(MasterView):
batch = self.handler.make_batch(session, **kwargs)
self.Session.flush()
# TODO: this needs work yet surely...
# if batch has input data file, let handler properly establish that
if 'filename' in form.schema:
if filedict:
self.handler.set_input_file(batch, filepath)
os.remove(filepath)
os.rmdir(tempdir)
self.process_uploads(batch, form, uploads)
return batch
def process_uploads(self, batch, form, uploads):
for key, upload in six.iteritems(uploads):
self.handler.set_input_file(batch, upload['temp_path'], attr=key)
os.remove(upload['temp_path'])
os.rmdir(upload['tempdir'])
def save_mobile_create_form(self, form):
self.before_create(form)
session = self.Session()
@ -536,6 +535,39 @@ class BatchMasterView(MasterView):
url = self.request.route_url('{}.delete_rows'.format(self.get_route_prefix()), uuid=batch.uuid)
return HTML.tag('p', c=[tags.link_to("Delete all rows matching current search", url)])
def make_row_grid_kwargs(self, **kwargs):
"""
Whether or not rows may be edited or deleted will depend partially on
whether the parent batch has been executed.
"""
batch = self.get_instance()
# TODO: most of this logic is copied from MasterView, should refactor/merge somehow...
if 'main_actions' not in kwargs:
actions = []
# view action
if self.rows_viewable:
view = lambda r, i: self.get_row_action_url('view', r)
actions.append(grids.GridAction('view', icon='zoomin', url=view))
# edit and delete are NOT allowed after execution, or if batch is "complete"
if not batch.executed and not batch.complete:
# edit action
if self.rows_editable:
actions.append(grids.GridAction('edit', icon='pencil', url=self.row_edit_action_url))
# delete action
permission_prefix = self.get_permission_prefix()
if self.rows_deletable and self.request.has_perm('{}.delete_row'.format(permission_prefix)):
actions.append(grids.GridAction('delete', icon='trash', url=self.row_delete_action_url))
kwargs.setdefault('delete_speedbump', self.rows_deletable_speedbump)
kwargs['main_actions'] = actions
return super(BatchMasterView, self).make_row_grid_kwargs(**kwargs)
def make_row_grid_tools(self, batch):
return (self.make_default_row_grid_tools(batch) or '') + (self.make_batch_row_grid_tools(batch) or '')
@ -555,10 +587,7 @@ class BatchMasterView(MasterView):
"""
Delete all data (files etc.) for the batch.
"""
if hasattr(batch, 'delete_data'):
batch.delete_data(self.rattail_config)
if hasattr(batch, 'data_rows'):
del batch.data_rows[:]
self.handler.delete(batch)
super(BatchMasterView, self).delete_instance(batch)
def get_fallback_templates(self, template, mobile=False):
@ -1153,6 +1182,7 @@ class FileBatchMasterView(BatchMasterView):
"""
Base class for all file-based "batch master" views.
"""
downloadable = True
@property
def upload_dir(self):
@ -1171,62 +1201,26 @@ class FileBatchMasterView(BatchMasterView):
def configure_form(self, f):
super(FileBatchMasterView, self).configure_form(f)
batch = f.model_instance
# filename
f.set_renderer('filename', self.render_filename)
f.set_label('filename', "Data File")
if self.editing:
f.set_readonly('filename')
if self.creating:
if 'filename' not in f.fields:
f.fields.insert(0, 'filename')
tmpstore = SessionFileUploadTempStore(self.request)
f.set_node('filename', colander.SchemaNode(deform.FileData(), widget=dfwidget.FileUploadWidget(tmpstore)))
# TODO: what's up with this re-insertion again..?
# if 'filename' not in f.fields:
# f.fields.insert(0, 'filename')
f.set_type('filename', 'file')
else:
f.set_readonly('filename')
f.set_renderer('filename', self.render_filename)
def render_filename(self, batch, field):
path = batch.filepath(self.rattail_config, filename=batch.filename)
filename = getattr(batch, field)
if not filename:
return ""
path = batch.filepath(self.rattail_config, filename=filename)
url = self.get_action_url('download', batch)
return self.render_file_field(path, url)
def download(self):
"""
View for downloading the data file associated with a batch.
"""
batch = self.get_instance()
if not batch:
raise httpexceptions.HTTPNotFound()
path = batch.filepath(self.rattail_config)
response = FileResponse(path, request=self.request)
response.headers[b'Content-Length'] = six.binary_type(os.path.getsize(path))
filename = os.path.basename(batch.filename).encode('ascii', 'replace')
response.headers[b'Content-Disposition'] = b'attachment; filename="{}"'.format(filename)
return response
@classmethod
def defaults(cls, config):
cls._filebatch_defaults(config)
cls._batch_defaults(config)
cls._defaults(config)
@classmethod
def _filebatch_defaults(cls, config):
route_prefix = cls.get_route_prefix()
url_prefix = cls.get_url_prefix()
permission_prefix = cls.get_permission_prefix()
model_title = cls.get_model_title()
model_title_plural = cls.get_model_title_plural()
# fix permission group title
config.add_tailbone_permission_group(permission_prefix, model_title_plural)
# download batch data file
config.add_route('{}.download'.format(route_prefix), '{}/{{uuid}}/download'.format(url_prefix))
config.add_view(cls, attr='download', route_name='{}.download'.format(route_prefix),
permission='{}.download'.format(permission_prefix))
config.add_tailbone_permission(permission_prefix, '{}.download'.format(permission_prefix),
"Download existing {} data file".format(model_title))
class MobileBatchStatusFilter(grids.filters.MobileFilter):

View file

@ -27,6 +27,7 @@ Model Master View
from __future__ import unicode_literals, absolute_import
import os
import tempfile
import logging
import six
@ -633,14 +634,39 @@ class MasterView(View):
return self.render_to_response('create', {'form': form}, mobile=True)
def save_create_form(self, form):
uploads = self.normalize_uploads(form)
self.before_create(form)
with self.Session().no_autoflush:
obj = self.objectify(form, self.form_deserialized)
self.before_create_flush(obj, form)
self.Session.add(obj)
self.Session.flush()
self.process_uploads(obj, form, uploads)
return obj
def normalize_uploads(self, form, skip=None):
uploads = {}
for node in form.schema:
if isinstance(node.typ, deform.FileData):
if skip and node.name in skip:
continue
filedict = self.form_deserialized.get(node.name)
if filedict:
tempdir = tempfile.mkdtemp()
filepath = os.path.join(tempdir, filedict['filename'])
tmpinfo = form.deform_form[node.name].widget.tmpstore.get(filedict['uid'])
tmpdata = tmpinfo['fp'].read()
with open(filepath, 'wb') as f:
f.write(tmpdata)
uploads[node.name] = {
'tempdir': tempdir,
'temp_path': filepath,
}
return uploads
def process_uploads(self, obj, form, uploads):
pass
def before_create_flush(self, obj, form):
pass
@ -1230,6 +1256,8 @@ class MasterView(View):
"""
obj = self.get_instance()
filename = self.request.GET.get('filename', None)
if not filename:
raise self.notfound()
path = self.download_path(obj, filename)
response = FileResponse(path, request=self.request)
response.content_length = os.path.getsize(path)
@ -2124,6 +2152,14 @@ class MasterView(View):
"""
return getattr(cls, 'mobile_row_form_factory', forms.Form)
def render_downloadable_file(self, obj, field):
filename = getattr(obj, field)
if not filename:
return ""
path = self.download_path(obj, filename)
url = self.get_action_url('download', obj, _query={'filename': filename})
return self.render_file_field(path, url)
def render_file_field(self, path, url=None, filename=None):
"""
Convenience for rendering a file with optional download link

View file

@ -26,6 +26,8 @@ Views for "true" purchase credits
from __future__ import unicode_literals, absolute_import
import six
from rattail.db import model
from webhelpers2.html import tags
@ -70,12 +72,13 @@ class PurchaseCreditView(MasterView):
g.set_sort_defaults('date_received', 'desc')
g.set_enum('status', self.enum.PURCHASE_CREDIT_STATUS)
g.filters['status'].set_value_renderer(grids.filters.EnumValueRenderer(self.enum.PURCHASE_CREDIT_STATUS))
g.filters['status'].default_active = True
g.filters['status'].default_verb = 'not_equal'
g.filters['status'].default_value = self.enum.PURCHASE_CREDIT_STATUS_SATISFIED
# TODO: should not have to convert value to string!
g.filters['status'].default_value = six.text_type(self.enum.PURCHASE_CREDIT_STATUS_SATISFIED)
g.set_enum('status', self.enum.PURCHASE_CREDIT_STATUS)
# g.set_type('upc', 'gpc')
g.set_type('cases_shorted', 'quantity')
g.set_type('units_shorted', 'quantity')

View file

@ -48,6 +48,7 @@ class PurchasingBatchView(BatchMasterView):
model_row_class = model.PurchaseBatchRow
default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler'
supports_new_product = False
cloneable = True
grid_columns = [
'id',
@ -513,22 +514,33 @@ class PurchasingBatchView(BatchMasterView):
kwargs = super(PurchasingBatchView, self).get_batch_kwargs(batch, mobile=mobile)
kwargs['mode'] = self.batch_mode
kwargs['truck_dump'] = batch.truck_dump
kwargs['invoice_parser_key'] = batch.invoice_parser_key
if batch.store:
kwargs['store'] = batch.store
elif batch.store_uuid:
kwargs['store_uuid'] = batch.store_uuid
if batch.truck_dump_batch:
kwargs['truck_dump_batch'] = batch.truck_dump_batch
elif batch.truck_dump_batch_uuid:
kwargs['truck_dump_batch_uuid'] = batch.truck_dump_batch_uuid
if batch.vendor:
kwargs['vendor'] = batch.vendor
elif batch.vendor_uuid:
kwargs['vendor_uuid'] = batch.vendor_uuid
if batch.department:
kwargs['department'] = batch.department
elif batch.department_uuid:
kwargs['department_uuid'] = batch.department_uuid
if batch.buyer:
kwargs['buyer'] = batch.buyer
elif batch.buyer_uuid:
kwargs['buyer_uuid'] = batch.buyer_uuid
kwargs['po_number'] = batch.po_number
kwargs['po_total'] = batch.po_total
@ -600,7 +612,9 @@ class PurchasingBatchView(BatchMasterView):
def row_grid_extra_class(self, row, i):
if row.status_code == row.STATUS_PRODUCT_NOT_FOUND:
return 'warning'
if row.status_code in (row.STATUS_INCOMPLETE, row.STATUS_ORDERED_RECEIVED_DIFFER):
if row.status_code in (row.STATUS_INCOMPLETE,
row.STATUS_ORDERED_RECEIVED_DIFFER,
row.STATUS_TRUCKDUMP_UNCLAIMED):
return 'notice'
def configure_row_form(self, f):

View file

@ -28,17 +28,19 @@ from __future__ import unicode_literals, absolute_import
import re
import six
import sqlalchemy as sa
from rattail import pod
from rattail.db import model, api
from rattail.gpc import GPC
from rattail.util import pretty_quantity, prettify
from rattail.vendors.invoices import iter_invoice_parsers, require_invoice_parser
import colander
from deform import widget as dfwidget
from pyramid import httpexceptions
from webhelpers2.html import tags
from webhelpers2.html import tags, HTML
from tailbone import forms, grids
from tailbone.views.purchasing import PurchasingBatchView
@ -96,9 +98,8 @@ class ReceivingBatchView(PurchasingBatchView):
model_title = "Receiving Batch"
model_title_plural = "Receiving Batches"
index_title = "Receiving"
creatable = False
downloadable = True
rows_editable = True
rows_deletable = False
mobile_creatable = True
mobile_rows_filterable = True
mobile_rows_creatable = True
@ -107,6 +108,11 @@ class ReceivingBatchView(PurchasingBatchView):
allow_from_scratch = True
allow_truck_dump = False
labels = {
'truck_dump_batch': "Truck Dump Parent",
'invoice_parser_key': "Invoice Parser",
}
grid_columns = [
'id',
'vendor',
@ -123,9 +129,14 @@ class ReceivingBatchView(PurchasingBatchView):
form_fields = [
'id',
'batch_type',
'store',
'vendor',
'truck_dump',
'truck_dump_children',
'truck_dump_batch',
'invoice_file',
'invoice_parser_key',
'department',
'purchase',
'vendor_email',
@ -143,6 +154,7 @@ class ReceivingBatchView(PurchasingBatchView):
'created',
'created_by',
'status_code',
'rowcount',
'complete',
'executed',
'executed_by',
@ -203,13 +215,167 @@ class ReceivingBatchView(PurchasingBatchView):
def batch_mode(self):
return self.enum.PURCHASE_BATCH_MODE_RECEIVING
def row_editable(self, row):
batch = row.batch
if batch.truck_dump_batch:
return False
return True
def row_deletable(self, row):
batch = row.batch
if batch.truck_dump:
return True
return False
def configure_form(self, f):
super(ReceivingBatchView, self).configure_form(f)
batch = f.model_instance
# batch_type
if self.creating:
batch_type_values = [
('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')
# truck_dump*
if self.allow_truck_dump:
# truck_dump
if self.editing:
if self.creating:
f.remove_field('truck_dump')
elif batch.truck_dump_batch:
f.remove_field('truck_dump')
else:
f.set_readonly('truck_dump')
# truck_dump_children
if self.viewing:
if batch.truck_dump:
f.set_renderer('truck_dump_children', self.render_truck_dump_children)
else:
f.remove_field('truck_dump_children')
else:
f.remove_field('truck_dump_children')
# truck_dump_batch
if self.creating:
f.replace('truck_dump_batch', 'truck_dump_batch_uuid')
batches = self.Session.query(model.PurchaseBatch)\
.filter(model.PurchaseBatch.mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING)\
.filter(model.PurchaseBatch.truck_dump == True)\
.filter(model.PurchaseBatch.complete == True)\
.filter(model.PurchaseBatch.executed == None)\
.order_by(model.PurchaseBatch.id)
batch_values = [(b.uuid, six.text_type(b)) for b in batches]
batch_values.insert(0, ('', "(please choose)"))
f.set_widget('truck_dump_batch_uuid', forms.widgets.JQuerySelectWidget(values=batch_values))
f.set_label('truck_dump_batch_uuid', "Truck Dump Parent")
elif batch.truck_dump:
f.remove_field('truck_dump_batch')
elif batch.truck_dump_batch:
f.set_readonly('truck_dump_batch')
f.set_renderer('truck_dump_batch', self.render_truck_dump_batch)
else:
f.remove_field('truck_dump_batch')
else:
f.remove_fields('truck_dump',
'truck_dump_children',
'truck_dump_batch')
# invoice_file
if self.creating:
f.set_type('invoice_file', 'file')
else:
f.set_readonly('invoice_file')
f.set_renderer('invoice_file', self.render_downloadable_file)
# invoice_parser_key
if self.creating:
parsers = sorted(iter_invoice_parsers(), key=lambda p: p.display)
parser_values = [(p.key, p.display) for p in parsers]
parser_values.insert(0, ('', "(please choose)"))
f.set_widget('invoice_parser_key', forms.widgets.JQuerySelectWidget(values=parser_values))
else:
f.remove_field('invoice_parser_key')
# store
if self.creating:
store = self.rattail_config.get_store(self.Session())
f.set_widget('store_uuid', forms.widgets.ReadonlyWidget())
f.set_default('store_uuid', store.uuid)
f.set_hidden('store_uuid')
# purchase
if self.creating:
f.remove_field('purchase')
# department
if self.creating:
f.remove_field('department_uuid')
def template_kwargs_create(self, **kwargs):
kwargs = super(ReceivingBatchView, self).template_kwargs_create(**kwargs)
if self.allow_truck_dump:
vmap = {}
batches = self.Session.query(model.PurchaseBatch)\
.filter(model.PurchaseBatch.mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING)\
.filter(model.PurchaseBatch.truck_dump == True)\
.filter(model.PurchaseBatch.complete == True)
for batch in batches:
vmap[batch.uuid] = batch.vendor_uuid
kwargs['batch_vendor_map'] = vmap
return kwargs
def get_batch_kwargs(self, batch, mobile=False):
kwargs = super(ReceivingBatchView, self).get_batch_kwargs(batch, mobile=mobile)
if not mobile:
batch_type = self.request.POST['batch_type']
if batch_type == 'from_scratch':
kwargs.pop('truck_dump_batch', None)
kwargs.pop('truck_dump_batch_uuid', None)
elif batch_type == 'truck_dump':
pass
else:
raise NotImplementedError
return kwargs
def delete_instance(self, batch):
"""
Delete all data (files etc.) for the batch.
"""
truck_dump = batch.truck_dump_batch
if batch.truck_dump:
for child in batch.truck_dump_children:
self.delete_instance(child)
super(ReceivingBatchView, self).delete_instance(batch)
if truck_dump:
self.handler.refresh(truck_dump)
def render_truck_dump_batch(self, batch, field):
truck_dump = batch.truck_dump_batch
if not truck_dump:
return ""
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_truck_dump_children(self, batch, field):
children = batch.truck_dump_children
if not children:
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)
def render_mobile_listitem(self, batch, i):
title = "({}) {} for ${:0,.2f} - {}, {}".format(
batch.id_str,