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 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django import forms
from InvenTree.forms import HelperForm from InvenTree.forms import HelperForm
from .models import InvenTreeSetting from .models import InvenTreeSetting

View File

@ -5,8 +5,12 @@ Django views for interacting with common models
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
import os
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.forms import CheckboxInput, Select from django.forms import CheckboxInput, Select
from django.conf import settings
from django.core.files.storage import FileSystemStorage
from formtools.wizard.views import SessionWizardView from formtools.wizard.views import SessionWizardView
@ -114,6 +118,17 @@ class MultiStepFormView(SessionWizardView):
form_list = [] form_list = []
form_steps_description = [] 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): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@ -126,4 +141,3 @@ class MultiStepFormView(SessionWizardView):
context.update({'description': description}) context.update({'description': description})
return context return context

View File

@ -14,6 +14,8 @@ from InvenTree.forms import HelperForm
from InvenTree.fields import RoundingDecimalFormField from InvenTree.fields import RoundingDecimalFormField
from InvenTree.fields import DatePickerFormField from InvenTree.fields import DatePickerFormField
from common.files import FileManager
import part.models import part.models
from stock.models import StockLocation from stock.models import StockLocation
@ -287,15 +289,29 @@ class EditSalesOrderAllocationForm(HelperForm):
class UploadFile(forms.Form): class UploadFile(forms.Form):
''' Step 1 ''' """ Step 1 """
first_name = forms.CharField(max_length=100) 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): class MatchField(forms.Form):
''' Step 2 ''' """ Step 2 """
last_name = forms.CharField(max_length=100) last_name = forms.CharField(max_length=100)
class MatchPart(forms.Form): class MatchPart(forms.Form):
''' Step 3 ''' """ Step 3 """
age = forms.IntegerField() age = forms.IntegerField()

View File

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

View File

@ -580,7 +580,7 @@ class PurchaseOrderUpload(MultiStepFormView):
_("Select Parts"), _("Select Parts"),
] ]
template_name = "order/po_upload.html" 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): def get_context_data(self, form, **kwargs):
context = super().get_context_data(form=form, **kwargs) context = super().get_context_data(form=form, **kwargs)
@ -591,6 +591,24 @@ class PurchaseOrderUpload(MultiStepFormView):
return context 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): def done(self, form_list, **kwargs):
return HttpResponseRedirect(reverse('po-detail', kwargs={'pk': self.kwargs['pk']})) return HttpResponseRedirect(reverse('po-detail', kwargs={'pk': self.kwargs['pk']}))