Added upload file step

This commit is contained in:
eeintech 2021-05-04 12:20:57 -04:00
parent 373898d43e
commit 7cdf0af04a
6 changed files with 241 additions and 11 deletions

184
InvenTree/common/files.py Normal file
View File

@ -0,0 +1,184 @@
"""
Files management tools.
"""
from rapidfuzz import fuzz
import tablib
import os
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
# from company.models import ManufacturerPart, SupplierPart
class FileManager:
""" Class for managing an uploaded file """
name = ''
# Fields which are absolutely necessary for valid upload
REQUIRED_HEADERS = [
'Quantity'
]
# Fields which are used for part matching (only one of them is needed)
PART_MATCH_HEADERS = [
'Part_Name',
'Part_IPN',
'Part_ID',
]
# Fields which would be helpful but are not required
OPTIONAL_HEADERS = [
]
EDITABLE_HEADERS = [
]
HEADERS = REQUIRED_HEADERS + PART_MATCH_HEADERS + OPTIONAL_HEADERS
def __init__(self, file, name=None):
""" Initialize the FileManager class with a user-uploaded file object """
# Set name
if name:
self.name = name
# Process initial file
self.process(file)
def process(self, file):
""" Process file """
self.data = None
ext = os.path.splitext(file.name)[-1].lower()
if ext in ['.csv', '.tsv', ]:
# These file formats need string decoding
raw_data = file.read().decode('utf-8')
elif ext in ['.xls', '.xlsx']:
raw_data = file.read()
else:
raise ValidationError(_(f'Unsupported file format: {ext}'))
try:
self.data = tablib.Dataset().load(raw_data)
except tablib.UnsupportedFormat:
raise ValidationError(_(f'Error reading {self.name} file (invalid format)'))
except tablib.core.InvalidDimensions:
raise ValidationError(_(f'Error reading {self.name} file (incorrect dimension)'))
def guess_header(self, header, threshold=80):
""" Try to match a header (from the file) to a list of known headers
Args:
header - Header name to look for
threshold - Match threshold for fuzzy search
"""
# Try for an exact match
for h in self.HEADERS:
if h == header:
return h
# Try for a case-insensitive match
for h in self.HEADERS:
if h.lower() == header.lower():
return h
# Try for a case-insensitive match with space replacement
for h in self.HEADERS:
if h.lower() == header.lower().replace(' ', '_'):
return h
# Finally, look for a close match using fuzzy matching
matches = []
for h in self.HEADERS:
ratio = fuzz.partial_ratio(header, h)
if ratio > threshold:
matches.append({'header': h, 'match': ratio})
if len(matches) > 0:
matches = sorted(matches, key=lambda item: item['match'], reverse=True)
return matches[0]['header']
return None
def columns(self):
""" Return a list of headers for the thingy """
headers = []
for header in self.data.headers:
headers.append({
'name': header,
'guess': self.guess_header(header)
})
return headers
def col_count(self):
if self.data is None:
return 0
return len(self.data.headers)
def row_count(self):
""" Return the number of rows in the file. """
if self.data is None:
return 0
return len(self.data)
def rows(self):
""" Return a list of all rows """
rows = []
for i in range(self.row_count()):
data = [item for item in self.get_row_data(i)]
# Is the row completely empty? Skip!
empty = True
for idx, item in enumerate(data):
if len(str(item).strip()) > 0:
empty = False
try:
# Excel import casts number-looking-items into floats, which is annoying
if item == int(item) and not str(item) == str(int(item)):
data[idx] = int(item)
except ValueError:
pass
# Skip empty rows
if empty:
continue
row = {
'data': data,
'index': i
}
rows.append(row)
return rows
def get_row_data(self, index):
""" Retrieve row data at a particular index """
if self.data is None or index >= len(self.data):
return None
return self.data[index]
def get_row_dict(self, index):
""" Retrieve a dict object representing the data row at a particular offset """
if self.data is None or index >= len(self.data):
return None
return self.data.dict[index]

View File

@ -5,8 +5,6 @@ Django forms for interacting with common objects
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django import forms
from InvenTree.forms import HelperForm
from .models import InvenTreeSetting

View File

@ -5,8 +5,12 @@ Django views for interacting with common models
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os
from django.utils.translation import ugettext_lazy as _
from django.forms import CheckboxInput, Select
from django.conf import settings
from django.core.files.storage import FileSystemStorage
from formtools.wizard.views import SessionWizardView
@ -106,7 +110,7 @@ class SettingEdit(AjaxUpdateView):
class MultiStepFormView(SessionWizardView):
""" Setup basic methods of multi-step form
""" Setup basic methods of multi-step form
form_list: list of forms
form_steps_description: description for each form
@ -114,6 +118,17 @@ class MultiStepFormView(SessionWizardView):
form_list = []
form_steps_description = []
media_folder = ''
file_storage = FileSystemStorage(settings.MEDIA_ROOT)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.media_folder:
media_folder_abs = os.path.join(settings.MEDIA_ROOT, self.media_folder)
if not os.path.exists(media_folder_abs):
os.mkdir(media_folder_abs)
self.file_storage = FileSystemStorage(location=media_folder_abs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@ -126,4 +141,3 @@ class MultiStepFormView(SessionWizardView):
context.update({'description': description})
return context

View File

@ -14,6 +14,8 @@ from InvenTree.forms import HelperForm
from InvenTree.fields import RoundingDecimalFormField
from InvenTree.fields import DatePickerFormField
from common.files import FileManager
import part.models
from stock.models import StockLocation
@ -287,15 +289,29 @@ class EditSalesOrderAllocationForm(HelperForm):
class UploadFile(forms.Form):
''' Step 1 '''
first_name = forms.CharField(max_length=100)
""" Step 1 """
file = forms.FileField(
label=_('Order File'),
help_text=_('Select order file to upload'),
)
file_manager = None
def clean_file(self):
file = self.cleaned_data['file']
# Create a FileManager object - will perform initial data validation
# (and raise a ValidationError if there is something wrong with the file)
self.file_manager = FileManager(file=file, name='order')
return file
class MatchField(forms.Form):
''' Step 2 '''
""" Step 2 """
last_name = forms.CharField(max_length=100)
class MatchPart(forms.Form):
''' Step 3 '''
age = forms.IntegerField()
""" Step 3 """
age = forms.IntegerField()

View File

@ -17,7 +17,7 @@
<p>{% trans "Step" %} {{ wizard.steps.step1 }} {% trans "of" %} {{ wizard.steps.count }}
{% if description %}- {{ description }}{% endif %}</p>
<form action="" method="post">{% csrf_token %}
<form action="" method="post" enctype="multipart/form-data">{% csrf_token %}
<table>
{% load crispy_forms_tags %}
{{ wizard.management_form }}

View File

@ -580,7 +580,7 @@ class PurchaseOrderUpload(MultiStepFormView):
_("Select Parts"),
]
template_name = "order/po_upload.html"
# file_storage = FileSystemStorage(location=os.path.join(settings.MEDIA_ROOT, 'file_uploads'))
media_folder = 'order_uploads/'
def get_context_data(self, form, **kwargs):
context = super().get_context_data(form=form, **kwargs)
@ -591,6 +591,24 @@ class PurchaseOrderUpload(MultiStepFormView):
return context
def get_form_step_data(self, form):
# print(f'{self.steps.current=}\n{form.data=}')
return form.data
def get_form_step_files(self, form):
# Check if user completed file upload
if self.steps.current == '0':
# Extract columns and rows from FileManager
self.extractDataFromFile(form.file_manager)
return form.files
def extractDataFromFile(self, file_manager):
""" Read data from the file """
self.columns = file_manager.columns()
self.rows = file_manager.rows()
def done(self, form_list, **kwargs):
return HttpResponseRedirect(reverse('po-detail', kwargs={'pk': self.kwargs['pk']}))