mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Render column selection options
- Guess which header is which
This commit is contained in:
parent
60050e9f32
commit
fb96651c15
@ -46,13 +46,9 @@ class BomUploadManager:
|
||||
""" Class for managing an uploaded BOM file """
|
||||
|
||||
# Fields which are absolutely necessary for valid upload
|
||||
REQUIRED_HEADERS = [
|
||||
HEADERS = [
|
||||
'Part',
|
||||
'Quantity',
|
||||
]
|
||||
|
||||
# Fields which are not necessary but can be populated
|
||||
USEFUL_HEADERS = [
|
||||
'Reference',
|
||||
'Overage',
|
||||
'Notes'
|
||||
@ -83,69 +79,49 @@ class BomUploadManager:
|
||||
except tablib.UnsupportedFormat:
|
||||
raise ValidationError({'bom_file': _('Error reading BOM file (invalid data)')})
|
||||
|
||||
# Now we have BOM data in memory!
|
||||
|
||||
self.header_map = {}
|
||||
|
||||
for header in self.REQUIRED_HEADERS:
|
||||
match = self.extract_header(header)
|
||||
if match is None:
|
||||
raise ValidationError({'bom_file': _("Missing required field '{f}'".format(f=header))})
|
||||
else:
|
||||
self.header_map[header] = match
|
||||
|
||||
for header in self.USEFUL_HEADERS:
|
||||
match = self.extract_header(header)
|
||||
|
||||
self.header_map[header] = match
|
||||
|
||||
def get_header(self, header_name):
|
||||
""" Returns the matching header name for the internal name """
|
||||
|
||||
if header_name in self.header_map.keys():
|
||||
return self.header_map[header_name]
|
||||
else:
|
||||
return None
|
||||
|
||||
def extract_header(self, header_name, threshold=80):
|
||||
""" Retrieve a matching column header from the uploaded file.
|
||||
If there is not an exact match, try to match one that is close.
|
||||
def guess_headers(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
|
||||
"""
|
||||
|
||||
headers = self.data.headers
|
||||
# Try for an exact match
|
||||
for h in self.HEADERS:
|
||||
if h == header:
|
||||
return h
|
||||
|
||||
# First, try for an exact match
|
||||
for header in headers:
|
||||
if header == header_name:
|
||||
return header
|
||||
|
||||
# Next, try for a case-insensitive match
|
||||
for header in headers:
|
||||
if header.lower() == header_name.lower():
|
||||
return header
|
||||
# Try for a case-insensitive match
|
||||
for h in self.HEADERS:
|
||||
if h.lower() == header.lower():
|
||||
return h
|
||||
|
||||
# Finally, look for a close match using fuzzy matching
|
||||
|
||||
matches = []
|
||||
|
||||
for header in headers:
|
||||
|
||||
ratio = fuzz.partial_ratio(header, header_name)
|
||||
for h in self.HEADERS:
|
||||
ratio = fuzz.partial_ratio(header, h)
|
||||
if ratio > threshold:
|
||||
matches.append({'header': header, 'match': ratio})
|
||||
matches.append({'header': h, 'match': ratio})
|
||||
|
||||
if len(matches) > 0:
|
||||
matches = sorted(matches, key=lambda item: item['match'], reverse=True)
|
||||
|
||||
# Return the field with the best match
|
||||
return matches[0]['header']
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_headers(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):
|
||||
|
@ -16,11 +16,24 @@
|
||||
<table class='table table-striped'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Row</th>
|
||||
{% for col in bom_cols %}
|
||||
<th>
|
||||
<select class='select' id='id_col_{{ forloop.counter0 }}' name='col_{{ forloop.counter0 }}'>
|
||||
<option value=''>---------</option>
|
||||
{% for req in req_cols %}
|
||||
<option value='{{ req }}'{% if req == col.guess %}selected='selected'{% endif %}>{{ req }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{{ col.name }}
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in bom.rows %}
|
||||
{% for row in bom_rows %}
|
||||
<tr>
|
||||
<td>{% add forloop.counter 1 %}</td>
|
||||
{% for item in row %}
|
||||
<td>
|
||||
{{ item }}
|
||||
|
@ -20,6 +20,12 @@ def multiply(x, y, *args, **kwargs):
|
||||
return x * y
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def add(x, y, *args, **kwargs):
|
||||
""" Add two numbers together """
|
||||
return x + y
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def part_allocation_count(build, part, *args, **kwargs):
|
||||
""" Return the total number of <part> allocated to <build> """
|
||||
|
@ -683,6 +683,13 @@ class BomUpload(AjaxView, FormMixin):
|
||||
return self.renderJsonResponse(request, self.form)
|
||||
|
||||
def handleBomFileUpload(self):
|
||||
""" Process a BOM file upload form.
|
||||
|
||||
This function validates that the uploaded file was valid,
|
||||
and contains tabulated data that can be extracted.
|
||||
If the file does not satisfy these requirements,
|
||||
the "upload file" form is again shown to the user.
|
||||
"""
|
||||
|
||||
bom_file = self.request.FILES.get('bom_file', None)
|
||||
|
||||
@ -713,13 +720,25 @@ class BomUpload(AjaxView, FormMixin):
|
||||
# BOM file is valid? Proceed to the next step!
|
||||
form = part_forms.BomUploadSelectFields
|
||||
self.ajax_template_name = 'part/bom_upload/select_fields.html'
|
||||
ctx['bom'] = manager
|
||||
|
||||
# Try to guess at the
|
||||
|
||||
# Provide context to the next form
|
||||
ctx = {
|
||||
'req_cols': BomUploadManager.HEADERS,
|
||||
'bom_cols': manager.get_headers(),
|
||||
'bom_rows': manager.rows(),
|
||||
}
|
||||
else:
|
||||
form = self.form
|
||||
|
||||
return self.renderJsonResponse(self.request, form, data=data, context=ctx)
|
||||
|
||||
def handleFieldSelection(self):
|
||||
""" Handle the output of the field selection form.
|
||||
Here the user is presented with the raw data and must select the
|
||||
column names and which rows to process.
|
||||
"""
|
||||
|
||||
data = {
|
||||
'form_valid': False,
|
||||
@ -727,7 +746,10 @@ class BomUpload(AjaxView, FormMixin):
|
||||
|
||||
self.ajax_template_name = 'part/bom_upload/select_fields.html'
|
||||
|
||||
ctx = {}
|
||||
ctx = {
|
||||
# The headers that we know about
|
||||
'known_headers': BomUploadManager.HEADERS,
|
||||
}
|
||||
|
||||
return self.renderJsonResponse(self.request, form=self.get_form(), data=data, context=ctx)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user