Add basic support for receiving from multiple invoice files
This commit is contained in:
parent
2b7ebedb22
commit
dfa4178204
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2022 Lance Edgar
|
# Copyright © 2010-2023 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -51,7 +51,9 @@ from webhelpers2.html import tags, HTML
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.util import raw_datetime, get_form_data, render_markdown
|
from tailbone.util import raw_datetime, get_form_data, render_markdown
|
||||||
from . import types
|
from . import types
|
||||||
from .widgets import ReadonlyWidget, PlainDateWidget, JQueryDateWidget, JQueryTimeWidget
|
from .widgets import (ReadonlyWidget, PlainDateWidget,
|
||||||
|
JQueryDateWidget, JQueryTimeWidget,
|
||||||
|
MultiFileUploadWidget)
|
||||||
from tailbone.exceptions import TailboneJSONFieldError
|
from tailbone.exceptions import TailboneJSONFieldError
|
||||||
|
|
||||||
|
|
||||||
|
@ -579,6 +581,10 @@ class Form(object):
|
||||||
node = colander.SchemaNode(nodeinfo, **kwargs)
|
node = colander.SchemaNode(nodeinfo, **kwargs)
|
||||||
self.nodes[key] = node
|
self.nodes[key] = node
|
||||||
|
|
||||||
|
# must explicitly replace node, if we already have a schema
|
||||||
|
if self.schema:
|
||||||
|
self.schema[key] = node
|
||||||
|
|
||||||
def set_type(self, key, type_, **kwargs):
|
def set_type(self, key, type_, **kwargs):
|
||||||
if type_ == 'datetime':
|
if type_ == 'datetime':
|
||||||
self.set_renderer(key, self.render_datetime)
|
self.set_renderer(key, self.render_datetime)
|
||||||
|
@ -624,9 +630,18 @@ class Form(object):
|
||||||
if 'required' in kwargs and not kwargs['required']:
|
if 'required' in kwargs and not kwargs['required']:
|
||||||
kw['missing'] = colander.null
|
kw['missing'] = colander.null
|
||||||
self.set_node(key, colander.SchemaNode(deform.FileData(), **kw))
|
self.set_node(key, colander.SchemaNode(deform.FileData(), **kw))
|
||||||
# must explicitly replace node, if we already have a schema
|
elif type_ == 'multi_file':
|
||||||
if self.schema:
|
tmpstore = SessionFileUploadTempStore(self.request)
|
||||||
self.schema[key] = self.nodes[key]
|
file_node = colander.SchemaNode(deform.FileData(),
|
||||||
|
name='upload')
|
||||||
|
|
||||||
|
kw = {'name': key,
|
||||||
|
'title': self.get_label(key),
|
||||||
|
'widget': MultiFileUploadWidget(tmpstore)}
|
||||||
|
# if 'required' in kwargs and not kwargs['required']:
|
||||||
|
# kw['missing'] = colander.null
|
||||||
|
files_node = colander.SequenceSchema(file_node, **kw)
|
||||||
|
self.set_node(key, files_node)
|
||||||
else:
|
else:
|
||||||
raise ValueError("unknown type for '{}' field: {}".format(key, type_))
|
raise ValueError("unknown type for '{}' field: {}".format(key, type_))
|
||||||
|
|
||||||
|
@ -853,25 +868,31 @@ class Form(object):
|
||||||
value = convert(field.cstruct)
|
value = convert(field.cstruct)
|
||||||
return json.dumps(value)
|
return json.dumps(value)
|
||||||
|
|
||||||
if isinstance(field.schema.typ, deform.FileData):
|
|
||||||
# TODO: we used to always/only return 'null' here but hopefully
|
|
||||||
# this also works, to show existing filename when present
|
|
||||||
if field.cstruct and field.cstruct['filename']:
|
|
||||||
return json.dumps({'name': field.cstruct['filename']})
|
|
||||||
return 'null'
|
|
||||||
|
|
||||||
if isinstance(field.schema.typ, colander.Set):
|
if isinstance(field.schema.typ, colander.Set):
|
||||||
if field.cstruct is colander.null:
|
if field.cstruct is colander.null:
|
||||||
return '[]'
|
return '[]'
|
||||||
|
|
||||||
if field.cstruct is colander.null:
|
|
||||||
return 'null'
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return json.dumps(field.cstruct)
|
return self.jsonify_value(field.cstruct)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
raise TailboneJSONFieldError(field.name, error)
|
raise TailboneJSONFieldError(field.name, error)
|
||||||
|
|
||||||
|
def jsonify_value(self, value):
|
||||||
|
"""
|
||||||
|
Take a Python value and convert to JSON
|
||||||
|
"""
|
||||||
|
if value is colander.null:
|
||||||
|
return 'null'
|
||||||
|
|
||||||
|
if isinstance(value, dfwidget.filedict):
|
||||||
|
# TODO: we used to always/only return 'null' here but hopefully
|
||||||
|
# this also works, to show existing filename when present
|
||||||
|
if value and value['filename']:
|
||||||
|
return json.dumps({'name': value['filename']})
|
||||||
|
return 'null'
|
||||||
|
|
||||||
|
return json.dumps(value)
|
||||||
|
|
||||||
def get_error_messages(self, field):
|
def get_error_messages(self, field):
|
||||||
if field.error:
|
if field.error:
|
||||||
return field.error.messages()
|
return field.error.messages()
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2022 Lance Edgar
|
# Copyright © 2010-2023 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -289,6 +289,79 @@ class JQueryAutocompleteWidget(dfwidget.AutocompleteInputWidget):
|
||||||
return field.renderer(template, **tmpl_values)
|
return field.renderer(template, **tmpl_values)
|
||||||
|
|
||||||
|
|
||||||
|
class MultiFileUploadWidget(dfwidget.FileUploadWidget):
|
||||||
|
"""
|
||||||
|
Widget to handle multiple (arbitrary number) of file uploads.
|
||||||
|
"""
|
||||||
|
template = 'multi_file_upload'
|
||||||
|
requirements = ()
|
||||||
|
|
||||||
|
def deserialize(self, field, pstruct):
|
||||||
|
if pstruct is colander.null:
|
||||||
|
return colander.null
|
||||||
|
|
||||||
|
# TODO: why is this a thing? pstruct == [b'']
|
||||||
|
if len(pstruct) == 1 and not pstruct[0]:
|
||||||
|
return colander.null
|
||||||
|
|
||||||
|
files_data = []
|
||||||
|
for upload in pstruct:
|
||||||
|
|
||||||
|
data = self.deserialize_upload(upload)
|
||||||
|
if data:
|
||||||
|
files_data.append(data)
|
||||||
|
|
||||||
|
if not files_data:
|
||||||
|
return colander.null
|
||||||
|
|
||||||
|
return files_data
|
||||||
|
|
||||||
|
def deserialize_upload(self, upload):
|
||||||
|
# nb. this logic was copied from parent class and adapted
|
||||||
|
# to allow for multiple files. needs some more love.
|
||||||
|
|
||||||
|
uid = None # TODO?
|
||||||
|
|
||||||
|
if hasattr(upload, "file"):
|
||||||
|
# the upload control had a file selected
|
||||||
|
data = dfwidget.filedict()
|
||||||
|
data["fp"] = upload.file
|
||||||
|
filename = upload.filename
|
||||||
|
# sanitize IE whole-path filenames
|
||||||
|
filename = filename[filename.rfind("\\") + 1 :].strip()
|
||||||
|
data["filename"] = filename
|
||||||
|
data["mimetype"] = upload.type
|
||||||
|
data["size"] = upload.length
|
||||||
|
if uid is None:
|
||||||
|
# no previous file exists
|
||||||
|
while 1:
|
||||||
|
uid = self.random_id()
|
||||||
|
if self.tmpstore.get(uid) is None:
|
||||||
|
data["uid"] = uid
|
||||||
|
self.tmpstore[uid] = data
|
||||||
|
preview_url = self.tmpstore.preview_url(uid)
|
||||||
|
self.tmpstore[uid]["preview_url"] = preview_url
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# a previous file exists
|
||||||
|
data["uid"] = uid
|
||||||
|
self.tmpstore[uid] = data
|
||||||
|
preview_url = self.tmpstore.preview_url(uid)
|
||||||
|
self.tmpstore[uid]["preview_url"] = preview_url
|
||||||
|
else:
|
||||||
|
# the upload control had no file selected
|
||||||
|
if uid is None:
|
||||||
|
# no previous file exists
|
||||||
|
return colander.null
|
||||||
|
else:
|
||||||
|
# a previous file should exist
|
||||||
|
data = self.tmpstore.get(uid)
|
||||||
|
# but if it doesn't, don't blow up
|
||||||
|
if data is None:
|
||||||
|
return colander.null
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
def make_customer_widget(request, **kwargs):
|
def make_customer_widget(request, **kwargs):
|
||||||
"""
|
"""
|
||||||
Make a customer widget; will be either autocomplete or dropdown
|
Make a customer widget; will be either autocomplete or dropdown
|
||||||
|
|
7
tailbone/templates/deform/multi_file_upload.pt
Normal file
7
tailbone/templates/deform/multi_file_upload.pt
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<tal:block tal:define="field_name field_name|field.name;
|
||||||
|
vmodel vmodel|'field_model_' + field_name;">
|
||||||
|
${field.start_sequence()}
|
||||||
|
<multi-file-upload v-model="${vmodel}">
|
||||||
|
</multi-file-upload>
|
||||||
|
${field.end_sequence()}
|
||||||
|
</tal:block>
|
60
tailbone/templates/multi_file_upload.mako
Normal file
60
tailbone/templates/multi_file_upload.mako
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
## -*- coding: utf-8; -*-
|
||||||
|
|
||||||
|
<%def name="render_template()">
|
||||||
|
<script type="text/x-template" id="multi-file-upload-template">
|
||||||
|
<section>
|
||||||
|
<b-field class="file">
|
||||||
|
<b-upload name="upload" multiple drag-drop expanded
|
||||||
|
v-model="files">
|
||||||
|
<section class="section">
|
||||||
|
<div class="content has-text-centered">
|
||||||
|
<p>
|
||||||
|
<b-icon icon="upload" size="is-large"></b-icon>
|
||||||
|
</p>
|
||||||
|
<p>Drop your files here or click to upload</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</b-upload>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<div class="tags" style="max-width: 40rem;">
|
||||||
|
<span v-for="(file, index) in files" :key="index" class="tag is-primary">
|
||||||
|
{{file.name}}
|
||||||
|
<button class="delete is-small" type="button"
|
||||||
|
@click="deleteFile(index)">
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</script>
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="declare_vars()">
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
let MultiFileUpload = {
|
||||||
|
template: '#multi-file-upload-template',
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
deleteFile(index) {
|
||||||
|
this.files.splice(index, 1);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
let MultiFileUploadData = {
|
||||||
|
files: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="make_component()">
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
MultiFileUpload.data = function() { return MultiFileUploadData }
|
||||||
|
|
||||||
|
Vue.component('multi-file-upload', MultiFileUpload)
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</%def>
|
|
@ -24,7 +24,16 @@
|
||||||
v-model="simpleSettings['rattail.batch.purchase.allow_receiving_from_invoice']"
|
v-model="simpleSettings['rattail.batch.purchase.allow_receiving_from_invoice']"
|
||||||
native-value="true"
|
native-value="true"
|
||||||
@input="settingsNeedSaved = true">
|
@input="settingsNeedSaved = true">
|
||||||
From Invoice
|
From Single Invoice
|
||||||
|
</b-checkbox>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field>
|
||||||
|
<b-checkbox name="rattail.batch.purchase.allow_receiving_from_multi_invoice"
|
||||||
|
v-model="simpleSettings['rattail.batch.purchase.allow_receiving_from_multi_invoice']"
|
||||||
|
native-value="true"
|
||||||
|
@input="settingsNeedSaved = true">
|
||||||
|
From Multiple (Combined) Invoices
|
||||||
</b-checkbox>
|
</b-checkbox>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
|
|
|
@ -81,12 +81,12 @@
|
||||||
<div class="panel-block">
|
<div class="panel-block">
|
||||||
<div style="display: flex;">
|
<div style="display: flex;">
|
||||||
<div>
|
<div>
|
||||||
|
${form.render_field_readonly('item_entry')}
|
||||||
% if row.product:
|
% if row.product:
|
||||||
${form.render_field_readonly(product_key_field)}
|
${form.render_field_readonly(product_key_field)}
|
||||||
${form.render_field_readonly('product')}
|
${form.render_field_readonly('product')}
|
||||||
% else:
|
% else:
|
||||||
${form.render_field_readonly(product_key_field)}
|
${form.render_field_readonly(product_key_field)}
|
||||||
${form.render_field_readonly('item_entry')}
|
|
||||||
% if product_key_field != 'upc':
|
% if product_key_field != 'upc':
|
||||||
${form.render_field_readonly('upc')}
|
${form.render_field_readonly('upc')}
|
||||||
% endif
|
% endif
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
<%namespace name="base_meta" file="/base_meta.mako" />
|
<%namespace name="base_meta" file="/base_meta.mako" />
|
||||||
<%namespace file="/formposter.mako" import="declare_formposter_mixin" />
|
<%namespace file="/formposter.mako" import="declare_formposter_mixin" />
|
||||||
<%namespace name="page_help" file="/page_help.mako" />
|
<%namespace name="page_help" file="/page_help.mako" />
|
||||||
|
<%namespace name="multi_file_upload" file="/multi_file_upload.mako" />
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
@ -577,6 +578,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
${tailbone_autocomplete_template()}
|
${tailbone_autocomplete_template()}
|
||||||
|
${multi_file_upload.render_template()}
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="render_this_page_component()">
|
<%def name="render_this_page_component()">
|
||||||
|
@ -764,6 +766,7 @@
|
||||||
|
|
||||||
<%def name="declare_whole_page_vars()">
|
<%def name="declare_whole_page_vars()">
|
||||||
${page_help.declare_vars()}
|
${page_help.declare_vars()}
|
||||||
|
${multi_file_upload.declare_vars()}
|
||||||
${h.javascript_link(request.static_url('tailbone:static/themes/falafel/js/tailbone.feedback.js') + '?ver={}'.format(tailbone.__version__))}
|
${h.javascript_link(request.static_url('tailbone:static/themes/falafel/js/tailbone.feedback.js') + '?ver={}'.format(tailbone.__version__))}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
@ -902,6 +905,7 @@
|
||||||
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.autocomplete.js') + '?ver={}'.format(tailbone.__version__))}
|
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.autocomplete.js') + '?ver={}'.format(tailbone.__version__))}
|
||||||
|
|
||||||
${page_help.make_component()}
|
${page_help.make_component()}
|
||||||
|
${multi_file_upload.make_component()}
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2022 Lance Edgar
|
# Copyright © 2010-2023 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -362,6 +362,7 @@ class BatchMasterView(MasterView):
|
||||||
f.remove('params')
|
f.remove('params')
|
||||||
else:
|
else:
|
||||||
f.set_readonly('params')
|
f.set_readonly('params')
|
||||||
|
f.set_renderer('params', self.render_params)
|
||||||
|
|
||||||
# created
|
# created
|
||||||
f.set_readonly('created')
|
f.set_readonly('created')
|
||||||
|
@ -419,6 +420,16 @@ class BatchMasterView(MasterView):
|
||||||
f.remove_fields('executed',
|
f.remove_fields('executed',
|
||||||
'executed_by')
|
'executed_by')
|
||||||
|
|
||||||
|
def render_params(self, batch, field):
|
||||||
|
params = self.get_visible_params(batch)
|
||||||
|
if not params:
|
||||||
|
return
|
||||||
|
|
||||||
|
return params
|
||||||
|
|
||||||
|
def get_visible_params(self, batch):
|
||||||
|
return dict(batch.params or {})
|
||||||
|
|
||||||
def render_complete(self, batch, field):
|
def render_complete(self, batch, field):
|
||||||
permission_prefix = self.get_permission_prefix()
|
permission_prefix = self.get_permission_prefix()
|
||||||
use_buefy = self.get_use_buefy()
|
use_buefy = self.get_use_buefy()
|
||||||
|
@ -515,11 +526,21 @@ class BatchMasterView(MasterView):
|
||||||
return batch
|
return batch
|
||||||
|
|
||||||
def process_uploads(self, batch, form, uploads):
|
def process_uploads(self, batch, form, uploads):
|
||||||
for key, upload in six.iteritems(uploads):
|
|
||||||
|
def process(upload, key):
|
||||||
self.handler.set_input_file(batch, upload['temp_path'], attr=key)
|
self.handler.set_input_file(batch, upload['temp_path'], attr=key)
|
||||||
os.remove(upload['temp_path'])
|
os.remove(upload['temp_path'])
|
||||||
os.rmdir(upload['tempdir'])
|
os.rmdir(upload['tempdir'])
|
||||||
|
|
||||||
|
for key, upload in six.iteritems(uploads):
|
||||||
|
if isinstance(upload, dict):
|
||||||
|
process(upload, key)
|
||||||
|
else:
|
||||||
|
uploads = upload
|
||||||
|
for upload in uploads:
|
||||||
|
if isinstance(upload, dict):
|
||||||
|
process(upload, key)
|
||||||
|
|
||||||
def get_batch_kwargs(self, batch, **kwargs):
|
def get_batch_kwargs(self, batch, **kwargs):
|
||||||
"""
|
"""
|
||||||
Return a kwargs dict for use with ``self.handler.make_batch()``, using
|
Return a kwargs dict for use with ``self.handler.make_batch()``, using
|
||||||
|
|
|
@ -53,6 +53,7 @@ from rattail.gpc import GPC
|
||||||
|
|
||||||
import colander
|
import colander
|
||||||
import deform
|
import deform
|
||||||
|
from deform import widget as dfwidget
|
||||||
from pyramid import httpexceptions
|
from pyramid import httpexceptions
|
||||||
from pyramid.renderers import get_renderer, render_to_response, render
|
from pyramid.renderers import get_renderer, render_to_response, render
|
||||||
from pyramid.response import FileResponse
|
from pyramid.response import FileResponse
|
||||||
|
@ -691,26 +692,40 @@ class MasterView(View):
|
||||||
|
|
||||||
def normalize_uploads(self, form, skip=None):
|
def normalize_uploads(self, form, skip=None):
|
||||||
uploads = {}
|
uploads = {}
|
||||||
for node in form.schema:
|
|
||||||
if isinstance(node.typ, deform.FileData):
|
def normalize(filedict):
|
||||||
if skip and node.name in skip:
|
|
||||||
continue
|
|
||||||
# TODO: does form ever *not* have 'validated' attr here?
|
|
||||||
if hasattr(form, 'validated'):
|
|
||||||
filedict = form.validated.get(node.name)
|
|
||||||
else:
|
|
||||||
filedict = self.form_deserialized.get(node.name)
|
|
||||||
if filedict:
|
|
||||||
tempdir = tempfile.mkdtemp()
|
tempdir = tempfile.mkdtemp()
|
||||||
filepath = os.path.join(tempdir, filedict['filename'])
|
filepath = os.path.join(tempdir, filedict['filename'])
|
||||||
tmpinfo = form.deform_form[node.name].widget.tmpstore.get(filedict['uid'])
|
tmpinfo = form.deform_form[node.name].widget.tmpstore.get(filedict['uid'])
|
||||||
tmpdata = tmpinfo['fp'].read()
|
tmpdata = tmpinfo['fp'].read()
|
||||||
with open(filepath, 'wb') as f:
|
with open(filepath, 'wb') as f:
|
||||||
f.write(tmpdata)
|
f.write(tmpdata)
|
||||||
uploads[node.name] = {
|
return {'tempdir': tempdir,
|
||||||
'tempdir': tempdir,
|
'temp_path': filepath}
|
||||||
'temp_path': filepath,
|
|
||||||
}
|
for node in form.schema:
|
||||||
|
if skip and node.name in skip:
|
||||||
|
continue
|
||||||
|
|
||||||
|
value = form.validated.get(node.name)
|
||||||
|
if not value:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isinstance(value, dfwidget.filedict):
|
||||||
|
uploads[node.name] = normalize(value)
|
||||||
|
|
||||||
|
elif not isinstance(value, dict):
|
||||||
|
|
||||||
|
try:
|
||||||
|
values = iter(value)
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
for value in values:
|
||||||
|
if isinstance(value, dfwidget.filedict):
|
||||||
|
uploads.setdefault(node.name, []).append(
|
||||||
|
normalize(value))
|
||||||
|
|
||||||
return uploads
|
return uploads
|
||||||
|
|
||||||
def process_uploads(self, obj, form, uploads):
|
def process_uploads(self, obj, form, uploads):
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2022 Lance Edgar
|
# Copyright © 2010-2023 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -26,6 +26,7 @@ Views for 'receiving' (purchasing) batches
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import decimal
|
import decimal
|
||||||
import logging
|
import logging
|
||||||
|
@ -551,6 +552,15 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
if not self.editing:
|
if not self.editing:
|
||||||
f.remove_field('order_quantities_known')
|
f.remove_field('order_quantities_known')
|
||||||
|
|
||||||
|
# multiple invoice files (if applicable)
|
||||||
|
if (not self.creating
|
||||||
|
and batch.get_param('receiving_workflow') == 'from_multi_invoice'):
|
||||||
|
|
||||||
|
if 'invoice_files' not in f:
|
||||||
|
f.insert_before('invoice_file', 'invoice_files')
|
||||||
|
f.set_renderer('invoice_files', self.render_invoice_files)
|
||||||
|
f.set_readonly('invoice_files', True)
|
||||||
|
|
||||||
# invoice totals
|
# invoice totals
|
||||||
f.set_label('invoice_total', "Invoice Total (Orig.)")
|
f.set_label('invoice_total', "Invoice Total (Orig.)")
|
||||||
f.set_label('invoice_total_calculated', "Invoice Total (Calc.)")
|
f.set_label('invoice_total_calculated', "Invoice Total (Calc.)")
|
||||||
|
@ -584,6 +594,17 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
'invoice_date',
|
'invoice_date',
|
||||||
'invoice_number')
|
'invoice_number')
|
||||||
|
|
||||||
|
elif workflow == 'from_multi_invoice':
|
||||||
|
if 'invoice_files' not in f:
|
||||||
|
f.insert_before('invoice_file', 'invoice_files')
|
||||||
|
f.set_type('invoice_files', 'multi_file')
|
||||||
|
f.set_required('invoice_parser_key')
|
||||||
|
f.remove('truck_dump_batch_uuid',
|
||||||
|
'po_number',
|
||||||
|
'invoice_file',
|
||||||
|
'invoice_date',
|
||||||
|
'invoice_number')
|
||||||
|
|
||||||
elif workflow == 'from_po':
|
elif workflow == 'from_po':
|
||||||
f.remove('truck_dump_batch_uuid',
|
f.remove('truck_dump_batch_uuid',
|
||||||
'date_ordered',
|
'date_ordered',
|
||||||
|
@ -620,12 +641,31 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
'invoice_date',
|
'invoice_date',
|
||||||
'invoice_number')
|
'invoice_number')
|
||||||
|
|
||||||
|
def render_invoice_files(self, batch, field):
|
||||||
|
datadir = self.batch_handler.datadir(batch)
|
||||||
|
items = []
|
||||||
|
for filename in batch.get_param('invoice_files', []):
|
||||||
|
path = os.path.join(datadir, filename)
|
||||||
|
url = self.get_action_url('download', batch,
|
||||||
|
_query={'filename': filename})
|
||||||
|
link = self.render_file_field(path, url)
|
||||||
|
items.append(HTML.tag('li', c=[link]))
|
||||||
|
return HTML.tag('ul', c=items)
|
||||||
|
|
||||||
def render_receiving_workflow(self, batch, field):
|
def render_receiving_workflow(self, batch, field):
|
||||||
key = self.request.matchdict['workflow_key']
|
key = self.request.matchdict['workflow_key']
|
||||||
info = self.handler.receiving_workflow_info(key)
|
info = self.handler.receiving_workflow_info(key)
|
||||||
if info:
|
if info:
|
||||||
return info['display']
|
return info['display']
|
||||||
|
|
||||||
|
def get_visible_params(self, batch):
|
||||||
|
params = super(ReceivingBatchView, self).get_visible_params(batch)
|
||||||
|
|
||||||
|
# remove this since we show it separately
|
||||||
|
params.pop('invoice_files', None)
|
||||||
|
|
||||||
|
return params
|
||||||
|
|
||||||
def template_kwargs_create(self, **kwargs):
|
def template_kwargs_create(self, **kwargs):
|
||||||
kwargs = super(ReceivingBatchView, self).template_kwargs_create(**kwargs)
|
kwargs = super(ReceivingBatchView, self).template_kwargs_create(**kwargs)
|
||||||
if self.handler.allow_truck_dump_receiving():
|
if self.handler.allow_truck_dump_receiving():
|
||||||
|
@ -655,6 +695,8 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
kwargs.pop('truck_dump_batch_uuid', None)
|
kwargs.pop('truck_dump_batch_uuid', None)
|
||||||
elif batch_type == 'from_invoice':
|
elif batch_type == 'from_invoice':
|
||||||
pass
|
pass
|
||||||
|
elif batch_type == 'from_multi_invoice':
|
||||||
|
pass
|
||||||
elif batch_type == 'from_po':
|
elif batch_type == 'from_po':
|
||||||
# TODO: how to best handle this field? this doesn't seem flexible
|
# TODO: how to best handle this field? this doesn't seem flexible
|
||||||
kwargs['purchase_key'] = batch.purchase_uuid
|
kwargs['purchase_key'] = batch.purchase_uuid
|
||||||
|
@ -1952,6 +1994,9 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
{'section': 'rattail.batch',
|
{'section': 'rattail.batch',
|
||||||
'option': 'purchase.allow_receiving_from_invoice',
|
'option': 'purchase.allow_receiving_from_invoice',
|
||||||
'type': bool},
|
'type': bool},
|
||||||
|
{'section': 'rattail.batch',
|
||||||
|
'option': 'purchase.allow_receiving_from_multi_invoice',
|
||||||
|
'type': bool},
|
||||||
{'section': 'rattail.batch',
|
{'section': 'rattail.batch',
|
||||||
'option': 'purchase.allow_receiving_from_purchase_order',
|
'option': 'purchase.allow_receiving_from_purchase_order',
|
||||||
'type': bool},
|
'type': bool},
|
||||||
|
|
Loading…
Reference in a new issue