Add basic support for receiving from multiple invoice files
This commit is contained in:
		
							parent
							
								
									2b7ebedb22
								
							
						
					
					
						commit
						dfa4178204
					
				
					 10 changed files with 295 additions and 40 deletions
				
			
		| 
						 | 
				
			
			@ -2,7 +2,7 @@
 | 
			
		|||
################################################################################
 | 
			
		||||
#
 | 
			
		||||
#  Rattail -- Retail Software Framework
 | 
			
		||||
#  Copyright © 2010-2022 Lance Edgar
 | 
			
		||||
#  Copyright © 2010-2023 Lance Edgar
 | 
			
		||||
#
 | 
			
		||||
#  This file is part of Rattail.
 | 
			
		||||
#
 | 
			
		||||
| 
						 | 
				
			
			@ -51,7 +51,9 @@ from webhelpers2.html import tags, HTML
 | 
			
		|||
from tailbone.db import Session
 | 
			
		||||
from tailbone.util import raw_datetime, get_form_data, render_markdown
 | 
			
		||||
from . import types
 | 
			
		||||
from .widgets import ReadonlyWidget, PlainDateWidget, JQueryDateWidget, JQueryTimeWidget
 | 
			
		||||
from .widgets import (ReadonlyWidget, PlainDateWidget,
 | 
			
		||||
                      JQueryDateWidget, JQueryTimeWidget,
 | 
			
		||||
                      MultiFileUploadWidget)
 | 
			
		||||
from tailbone.exceptions import TailboneJSONFieldError
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -579,6 +581,10 @@ class Form(object):
 | 
			
		|||
            node = colander.SchemaNode(nodeinfo, **kwargs)
 | 
			
		||||
        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):
 | 
			
		||||
        if type_ == 'datetime':
 | 
			
		||||
            self.set_renderer(key, self.render_datetime)
 | 
			
		||||
| 
						 | 
				
			
			@ -624,9 +630,18 @@ class Form(object):
 | 
			
		|||
            if 'required' in kwargs and not kwargs['required']:
 | 
			
		||||
                kw['missing'] = colander.null
 | 
			
		||||
            self.set_node(key, colander.SchemaNode(deform.FileData(), **kw))
 | 
			
		||||
            # must explicitly replace node, if we already have a schema
 | 
			
		||||
            if self.schema:
 | 
			
		||||
                self.schema[key] = self.nodes[key]
 | 
			
		||||
        elif type_ == 'multi_file':
 | 
			
		||||
            tmpstore = SessionFileUploadTempStore(self.request)
 | 
			
		||||
            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:
 | 
			
		||||
            raise ValueError("unknown type for '{}' field: {}".format(key, type_))
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -853,25 +868,31 @@ class Form(object):
 | 
			
		|||
            value = convert(field.cstruct)
 | 
			
		||||
            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 field.cstruct is colander.null:
 | 
			
		||||
                return '[]'
 | 
			
		||||
 | 
			
		||||
        if field.cstruct is colander.null:
 | 
			
		||||
            return 'null'
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            return json.dumps(field.cstruct)
 | 
			
		||||
            return self.jsonify_value(field.cstruct)
 | 
			
		||||
        except Exception as 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):
 | 
			
		||||
        if field.error:
 | 
			
		||||
            return field.error.messages()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@
 | 
			
		|||
################################################################################
 | 
			
		||||
#
 | 
			
		||||
#  Rattail -- Retail Software Framework
 | 
			
		||||
#  Copyright © 2010-2022 Lance Edgar
 | 
			
		||||
#  Copyright © 2010-2023 Lance Edgar
 | 
			
		||||
#
 | 
			
		||||
#  This file is part of Rattail.
 | 
			
		||||
#
 | 
			
		||||
| 
						 | 
				
			
			@ -289,6 +289,79 @@ class JQueryAutocompleteWidget(dfwidget.AutocompleteInputWidget):
 | 
			
		|||
        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):
 | 
			
		||||
    """
 | 
			
		||||
    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']"
 | 
			
		||||
                  native-value="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-field>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -81,12 +81,12 @@
 | 
			
		|||
          <div class="panel-block">
 | 
			
		||||
            <div style="display: flex;">
 | 
			
		||||
              <div>
 | 
			
		||||
                ${form.render_field_readonly('item_entry')}
 | 
			
		||||
                % if row.product:
 | 
			
		||||
                    ${form.render_field_readonly(product_key_field)}
 | 
			
		||||
                    ${form.render_field_readonly('product')}
 | 
			
		||||
                % else:
 | 
			
		||||
                    ${form.render_field_readonly(product_key_field)}
 | 
			
		||||
                    ${form.render_field_readonly('item_entry')}
 | 
			
		||||
                    % if product_key_field != 'upc':
 | 
			
		||||
                        ${form.render_field_readonly('upc')}
 | 
			
		||||
                    % endif
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,6 +4,7 @@
 | 
			
		|||
<%namespace name="base_meta" file="/base_meta.mako" />
 | 
			
		||||
<%namespace file="/formposter.mako" import="declare_formposter_mixin" />
 | 
			
		||||
<%namespace name="page_help" file="/page_help.mako" />
 | 
			
		||||
<%namespace name="multi_file_upload" file="/multi_file_upload.mako" />
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
  <head>
 | 
			
		||||
| 
						 | 
				
			
			@ -577,6 +578,7 @@
 | 
			
		|||
  </script>
 | 
			
		||||
 | 
			
		||||
  ${tailbone_autocomplete_template()}
 | 
			
		||||
  ${multi_file_upload.render_template()}
 | 
			
		||||
</%def>
 | 
			
		||||
 | 
			
		||||
<%def name="render_this_page_component()">
 | 
			
		||||
| 
						 | 
				
			
			@ -764,6 +766,7 @@
 | 
			
		|||
 | 
			
		||||
<%def name="declare_whole_page_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__))}
 | 
			
		||||
  <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__))}
 | 
			
		||||
 | 
			
		||||
  ${page_help.make_component()}
 | 
			
		||||
  ${multi_file_upload.make_component()}
 | 
			
		||||
 | 
			
		||||
  <script type="text/javascript">
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@
 | 
			
		|||
################################################################################
 | 
			
		||||
#
 | 
			
		||||
#  Rattail -- Retail Software Framework
 | 
			
		||||
#  Copyright © 2010-2022 Lance Edgar
 | 
			
		||||
#  Copyright © 2010-2023 Lance Edgar
 | 
			
		||||
#
 | 
			
		||||
#  This file is part of Rattail.
 | 
			
		||||
#
 | 
			
		||||
| 
						 | 
				
			
			@ -362,6 +362,7 @@ class BatchMasterView(MasterView):
 | 
			
		|||
            f.remove('params')
 | 
			
		||||
        else:
 | 
			
		||||
            f.set_readonly('params')
 | 
			
		||||
            f.set_renderer('params', self.render_params)
 | 
			
		||||
 | 
			
		||||
        # created
 | 
			
		||||
        f.set_readonly('created')
 | 
			
		||||
| 
						 | 
				
			
			@ -419,6 +420,16 @@ class BatchMasterView(MasterView):
 | 
			
		|||
                f.remove_fields('executed',
 | 
			
		||||
                                '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):
 | 
			
		||||
        permission_prefix = self.get_permission_prefix()
 | 
			
		||||
        use_buefy = self.get_use_buefy()
 | 
			
		||||
| 
						 | 
				
			
			@ -515,11 +526,21 @@ class BatchMasterView(MasterView):
 | 
			
		|||
        return batch
 | 
			
		||||
 | 
			
		||||
    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)
 | 
			
		||||
            os.remove(upload['temp_path'])
 | 
			
		||||
            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):
 | 
			
		||||
        """
 | 
			
		||||
        Return a kwargs dict for use with ``self.handler.make_batch()``, using
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -53,6 +53,7 @@ from rattail.gpc import GPC
 | 
			
		|||
 | 
			
		||||
import colander
 | 
			
		||||
import deform
 | 
			
		||||
from deform import widget as dfwidget
 | 
			
		||||
from pyramid import httpexceptions
 | 
			
		||||
from pyramid.renderers import get_renderer, render_to_response, render
 | 
			
		||||
from pyramid.response import FileResponse
 | 
			
		||||
| 
						 | 
				
			
			@ -691,26 +692,40 @@ class MasterView(View):
 | 
			
		|||
 | 
			
		||||
    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
 | 
			
		||||
                # 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:
 | 
			
		||||
 | 
			
		||||
        def normalize(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 {'tempdir': tempdir,
 | 
			
		||||
                    '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
 | 
			
		||||
 | 
			
		||||
    def process_uploads(self, obj, form, uploads):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@
 | 
			
		|||
################################################################################
 | 
			
		||||
#
 | 
			
		||||
#  Rattail -- Retail Software Framework
 | 
			
		||||
#  Copyright © 2010-2022 Lance Edgar
 | 
			
		||||
#  Copyright © 2010-2023 Lance Edgar
 | 
			
		||||
#
 | 
			
		||||
#  This file is part of Rattail.
 | 
			
		||||
#
 | 
			
		||||
| 
						 | 
				
			
			@ -26,6 +26,7 @@ Views for 'receiving' (purchasing) batches
 | 
			
		|||
 | 
			
		||||
from __future__ import unicode_literals, absolute_import
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
import re
 | 
			
		||||
import decimal
 | 
			
		||||
import logging
 | 
			
		||||
| 
						 | 
				
			
			@ -551,6 +552,15 @@ class ReceivingBatchView(PurchasingBatchView):
 | 
			
		|||
        if not self.editing:
 | 
			
		||||
            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
 | 
			
		||||
        f.set_label('invoice_total', "Invoice Total (Orig.)")
 | 
			
		||||
        f.set_label('invoice_total_calculated', "Invoice Total (Calc.)")
 | 
			
		||||
| 
						 | 
				
			
			@ -584,6 +594,17 @@ class ReceivingBatchView(PurchasingBatchView):
 | 
			
		|||
                         'invoice_date',
 | 
			
		||||
                         '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':
 | 
			
		||||
                f.remove('truck_dump_batch_uuid',
 | 
			
		||||
                         'date_ordered',
 | 
			
		||||
| 
						 | 
				
			
			@ -620,12 +641,31 @@ class ReceivingBatchView(PurchasingBatchView):
 | 
			
		|||
                         'invoice_date',
 | 
			
		||||
                         '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):
 | 
			
		||||
        key = self.request.matchdict['workflow_key']
 | 
			
		||||
        info = self.handler.receiving_workflow_info(key)
 | 
			
		||||
        if info:
 | 
			
		||||
            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):
 | 
			
		||||
        kwargs = super(ReceivingBatchView, self).template_kwargs_create(**kwargs)
 | 
			
		||||
        if self.handler.allow_truck_dump_receiving():
 | 
			
		||||
| 
						 | 
				
			
			@ -655,6 +695,8 @@ class ReceivingBatchView(PurchasingBatchView):
 | 
			
		|||
            kwargs.pop('truck_dump_batch_uuid', None)
 | 
			
		||||
        elif batch_type == 'from_invoice':
 | 
			
		||||
            pass
 | 
			
		||||
        elif batch_type == 'from_multi_invoice':
 | 
			
		||||
            pass
 | 
			
		||||
        elif batch_type == 'from_po':
 | 
			
		||||
            # TODO: how to best handle this field?  this doesn't seem flexible
 | 
			
		||||
            kwargs['purchase_key'] = batch.purchase_uuid
 | 
			
		||||
| 
						 | 
				
			
			@ -1952,6 +1994,9 @@ class ReceivingBatchView(PurchasingBatchView):
 | 
			
		|||
            {'section': 'rattail.batch',
 | 
			
		||||
             'option': 'purchase.allow_receiving_from_invoice',
 | 
			
		||||
             'type': bool},
 | 
			
		||||
            {'section': 'rattail.batch',
 | 
			
		||||
             'option': 'purchase.allow_receiving_from_multi_invoice',
 | 
			
		||||
             'type': bool},
 | 
			
		||||
            {'section': 'rattail.batch',
 | 
			
		||||
             'option': 'purchase.allow_receiving_from_purchase_order',
 | 
			
		||||
             'type': bool},
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue