diff --git a/InvenTree/part/bom.py b/InvenTree/part/bom.py index 5492ac0a0c..5108672e66 100644 --- a/InvenTree/part/bom.py +++ b/InvenTree/part/bom.py @@ -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): diff --git a/InvenTree/part/templates/part/bom_upload/select_fields.html b/InvenTree/part/templates/part/bom_upload/select_fields.html index 0656a957a5..709cf7a18d 100644 --- a/InvenTree/part/templates/part/bom_upload/select_fields.html +++ b/InvenTree/part/templates/part/bom_upload/select_fields.html @@ -16,11 +16,24 @@ + + {% for col in bom_cols %} + + {% endfor %} - {% for row in bom.rows %} + {% for row in bom_rows %} + {% for item in row %}
Row + + {{ col.name }} +
{% add forloop.counter 1 %} {{ item }} diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py index 696edbada1..c51d07fdd5 100644 --- a/InvenTree/part/templatetags/inventree_extras.py +++ b/InvenTree/part/templatetags/inventree_extras.py @@ -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 allocated to """ diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 94d433a30a..ad55f75f3d 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -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)