From 2dc43f0cf1c2877e4ce9c98cb57b53d38b1cfd4d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 27 Jun 2019 21:08:49 +1000 Subject: [PATCH 01/70] no message --- InvenTree/part/templates/part/bom.html | 48 ++++++++++++------------ InvenTree/part/views.py | 32 ---------------- InvenTree/static/script/inventree/bom.js | 48 +----------------------- 3 files changed, 26 insertions(+), 102 deletions(-) diff --git a/InvenTree/part/templates/part/bom.html b/InvenTree/part/templates/part/bom.html index dee1b0f140..3133aa1706 100644 --- a/InvenTree/part/templates/part/bom.html +++ b/InvenTree/part/templates/part/bom.html @@ -31,25 +31,25 @@ {% endif %} -
+
{% if editing_enabled %} -
- - -
+ + + {% else %} - @@ -76,6 +76,12 @@ sub_part_detail: true, }); + linkButtonsToSelection($("#bom-table"), + [ + "#bom-item-delete", + ] + ); + {% if editing_enabled %} $("#editing-finished").click(function() { location.href = "{% url 'part-bom' part.id %}"; @@ -115,12 +121,8 @@ location.href = "{% url 'part-bom' part.id %}?edit=True"; }); - $("#export-bom").click(function () { - downloadBom({ - modal: '#modal-form', - url: "{% url 'bom-export' part.id %}" - }); - + $(".download-bom").click(function () { + location.href = "{% url 'bom-export' part.id %}?format=" + $(this).attr('format'); }); {% endif %} diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 83989b1dde..49fa2bd8dd 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -616,38 +616,6 @@ class BomValidate(AjaxUpdateView): return self.renderJsonResponse(request, form, data, context=self.get_context()) -class BomExport(AjaxView): - - model = Part - ajax_form_title = 'Export BOM' - ajax_template_name = 'part/bom_export.html' - form_class = part_forms.BomExportForm - - def get_object(self): - return get_object_or_404(Part, pk=self.kwargs['pk']) - - def get(self, request, *args, **kwargs): - form = self.form_class() - - return self.renderJsonResponse(request, form) - - def post(self, request, *args, **kwargs): - """ - User has now submitted the BOM export data - """ - - # part = self.get_object() - - return super(AjaxView, self).post(request, *args, **kwargs) - - def get_data(self): - return { - # 'form_valid': True, - # 'redirect': '/' - # 'redirect': reverse('bom-download', kwargs={'pk': self.request.GET.get('pk')}) - } - - class BomDownload(AjaxView): """ Provide raw download of a BOM file. diff --git a/InvenTree/static/script/inventree/bom.js b/InvenTree/static/script/inventree/bom.js index 28b6dc4b73..8de9f5ce20 100644 --- a/InvenTree/static/script/inventree/bom.js +++ b/InvenTree/static/script/inventree/bom.js @@ -12,52 +12,6 @@ function reloadBomTable(table, options) { } -function downloadBom(options = {}) { - - var modal = options.modal || "#modal-form"; - - var content = ` - Select file format
-
- -
- `; - - openModal({ - modal: modal, - title: "Export Bill of Materials", - submit_text: "Download", - close_text: "Cancel", - }); - - modalSetContent(modal, content); - - modalEnable(modal, true); - - $(modal).on('click', '#modal-form-submit', function() { - $(modal).modal('hide'); - - var format = $(modal).find('#bom-format :selected').val(); - - if (options.url) { - var url = options.url + "?format=" + format; - - location.href = url; - } - }); -} - - function loadBomTable(table, options) { /* Load a BOM table with some configurable options. * @@ -192,7 +146,7 @@ function loadBomTable(table, options) { var bEdit = ""; var bDelt = ""; - return "
" + bEdit + bDelt + "
"; + return "
" + bEdit + bDelt + "
"; } }); } From 8b207d0d1ded2bb0a307dca388339f4a815f48d7 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 24 May 2019 23:56:36 +1000 Subject: [PATCH 02/70] Initial work towards uploading a BOM file - Create a form with a single FileField --- InvenTree/part/forms.py | 17 ++----- InvenTree/part/templates/part/bom.html | 7 +++ InvenTree/part/urls.py | 1 + InvenTree/part/views.py | 62 ++++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 13 deletions(-) diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index 1564c16316..4512859828 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -38,24 +38,15 @@ class BomValidateForm(HelperForm): ] -class BomExportForm(HelperForm): +class BomImportForm(HelperForm): + """ Form for importing a BOM. Provides a file input box for upload """ - # TODO - Define these choices somewhere else, and import them here - format_choices = ( - ('csv', 'CSV'), - ('pdf', 'PDF'), - ('xml', 'XML'), - ('xlsx', 'XLSX'), - ('html', 'HTML') - ) - - # Select export type - format = forms.CharField(label='Format', widget=forms.Select(choices=format_choices), required='true', help_text='Select export format') + bom_file = forms.FileField(label='BOM file', required=True, help_text="Select BOM file to upload") class Meta: model = Part fields = [ - 'format', + 'bom_file', ] diff --git a/InvenTree/part/templates/part/bom.html b/InvenTree/part/templates/part/bom.html index 3133aa1706..04101ab995 100644 --- a/InvenTree/part/templates/part/bom.html +++ b/InvenTree/part/templates/part/bom.html @@ -34,6 +34,7 @@
{% if editing_enabled %} + {% else %} @@ -106,6 +107,12 @@ ); }); + $("#bom-upload").click(function() { + launchModalForm( + "{% url 'bom-import' part.id %}", + ); + }); + {% else %} $("#validate-bom").click(function() { diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index 7dc53372cd..a8f5184972 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -22,6 +22,7 @@ part_detail_urls = [ url(r'^edit/?', views.PartEdit.as_view(), name='part-edit'), url(r'^delete/?', views.PartDelete.as_view(), name='part-delete'), url(r'^bom-export/?', views.BomDownload.as_view(), name='bom-export'), + url(r'^bom-import/?', views.BomUpload.as_view(), name='bom-import'), url(r'^validate-bom/', views.BomValidate.as_view(), name='bom-validate'), url(r'^duplicate/', views.PartDuplicate.as_view(), name='part-duplicate'), url(r'^make-variant/', views.MakePartVariant.as_view(), name='make-part-variant'), diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 49fa2bd8dd..12c2b808d8 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -616,6 +616,68 @@ class BomValidate(AjaxUpdateView): return self.renderJsonResponse(request, form, data, context=self.get_context()) +class BomUpload(AjaxView): + """ View for uploading a BOM file, and handling BOM data importing. + + The BOM upload process is as follows: + + 1. (Client) Select and upload BOM file + 2. (Server) Verify that supplied file is a file compatible with tablib library + 3. (Server) Introspect data file, try to find sensible columns / values / etc + 4. (Server) Send suggestions back to the client + 5. (Client) Makes choices based on suggestions: + - Accept automatic matching to parts found in database + - Accept suggestions for 'partial' or 'fuzzy' matches + - Create new parts in case of parts not being available + 6. (Client) Sends updated dataset back to server + 7. (Server) Check POST data for validity, sanity checking, etc. + 8. (Server) Respond to POST request + - If data are valid, proceed to 9. + - If data not valid, return to 4. + 9. (Server) Send confirmation form to user + - Display the actions which will occur + - Provide final "CONFIRM" button + 10. (Client) Confirm final changes + 11. (Server) Apply changes to database, update BOM items. + + During these steps, data are passed between the server/client as JSON objects. + """ + + def get_form(self): + """ Return the correct form for the given step in the upload process """ + + if 1 or self.request.method == 'GET': + form = part_forms.BomImportForm() + + return form + + def get(self, request, *args, **kwargs): + """ Perform the initial 'GET' request. + + Initially returns a form for file upload """ + + # A valid Part object must be supplied. This is the 'parent' part for the BOM + part = get_object_or_404(Part, pk=self.kwargs['pk']) + + form = self.get_form() + + return self.renderJsonResponse(request, form) + + def post(self, request, *args, **kwargs): + """ Perform the various 'POST' requests required. + """ + + data = { + 'form_valid': False, + } + + form = self.get_form() + + form.errors['bom_file'] = ['Nah mate'] + + return self.renderJsonResponse(request, form, data=data) + + class BomDownload(AjaxView): """ Provide raw download of a BOM file. From 86695cf2bb473dd4269bf8fd9924705722f02f84 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 25 May 2019 00:18:04 +1000 Subject: [PATCH 03/70] Validate uploaded BOM file with tablib - Check to see if a bom_file file object was uploaded - Pass off to the BOM file validity checker - Only a valid tabulated dataset will be accepted --- InvenTree/part/views.py | 52 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 12c2b808d8..95e0ed4946 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -5,6 +5,9 @@ Django views for interacting with Part app # -*- coding: utf-8 -*- from __future__ import unicode_literals +import tablib +import os + from django.shortcuts import get_object_or_404 from django.utils.translation import gettext_lazy as _ from django.urls import reverse_lazy @@ -656,6 +659,8 @@ class BomUpload(AjaxView): Initially returns a form for file upload """ + self.request = request + # A valid Part object must be supplied. This is the 'parent' part for the BOM part = get_object_or_404(Part, pk=self.kwargs['pk']) @@ -663,17 +668,56 @@ class BomUpload(AjaxView): return self.renderJsonResponse(request, form) + def handleBomFileUpload(self, bom_file): + + form = part_forms.BomImportForm() + + data = { + # TODO - Validate the form if there isn't actually an error! + 'form_valid': False + } + + # Extract the file format data + ext = os.path.splitext(bom_file.name)[-1].lower() + + if ext in ['.csv', '.tsv', ]: + # These file formats need string decoding + raw_data = bom_file.read().decode('utf-8') + elif ext in ['.xls', '.xlsx']: + raw_data = bom_file.read() + else: + form.errors['bom_file'] = ['Unsupported file format: ' + ext] + return self.renderJsonResponse(self.request, form, data) + + # Now try to read the data + try: + bom_data = tablib.Dataset().load(raw_data) + print(bom_data) + except tablib.UnsupportedFormat: + valid = False + form.errors['bom_file'] = [ + "Error reading '{f}' (Unsupported file format)".format(f=str(bom_file)), + ] + + return self.renderJsonResponse(self.request, form, data=data) + def post(self, request, *args, **kwargs): """ Perform the various 'POST' requests required. """ - data = { - 'form_valid': False, - } + self.request = request + + # Did the user POST a file named bom_file? + bom_file = request.FILES.get('bom_file', None) form = self.get_form() - form.errors['bom_file'] = ['Nah mate'] + if bom_file: + return self.handleBomFileUpload(bom_file) + + data = { + 'form_valid': False, + } return self.renderJsonResponse(request, form, data=data) From 686a61fba9a8cc523aee6edd21d97a8491920e55 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 25 May 2019 00:36:02 +1000 Subject: [PATCH 04/70] Hide checkboxes when BOM is not being edited --- InvenTree/static/script/inventree/bom.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/InvenTree/static/script/inventree/bom.js b/InvenTree/static/script/inventree/bom.js index 8de9f5ce20..0f3134c193 100644 --- a/InvenTree/static/script/inventree/bom.js +++ b/InvenTree/static/script/inventree/bom.js @@ -32,13 +32,16 @@ function loadBomTable(table, options) { title: 'ID', visible: false, }, - { + ]; + + if (options.editable) { + cols.push({ checkbox: true, title: 'Select', searchable: false, sortable: false, - }, - ]; + }); + } // Part column cols.push( From 72486448b8a7f8773508d65afb2c8bf3763c350a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 27 Jun 2019 21:17:33 +1000 Subject: [PATCH 05/70] Fix form title --- InvenTree/part/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 95e0ed4946..08e3be79cb 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -646,6 +646,8 @@ class BomUpload(AjaxView): During these steps, data are passed between the server/client as JSON objects. """ + ajax_form_title = 'Upload Bill of Materials' + def get_form(self): """ Return the correct form for the given step in the upload process """ From 3085db44afe520aeba4125d5b732838e323148da Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 27 Jun 2019 21:44:40 +1000 Subject: [PATCH 06/70] Add 'reference' field to BOM item model --- .../migrations/0012_auto_20190627_2144.py | 23 +++++++++++++++++++ InvenTree/part/models.py | 11 +++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 InvenTree/part/migrations/0012_auto_20190627_2144.py diff --git a/InvenTree/part/migrations/0012_auto_20190627_2144.py b/InvenTree/part/migrations/0012_auto_20190627_2144.py new file mode 100644 index 0000000000..ffd574b61d --- /dev/null +++ b/InvenTree/part/migrations/0012_auto_20190627_2144.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.2 on 2019-06-27 11:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0011_part_revision'), + ] + + operations = [ + migrations.AddField( + model_name='bomitem', + name='reference', + field=models.CharField(blank=True, help_text='BOM item reference', max_length=500), + ), + migrations.AlterField( + model_name='bomitem', + name='note', + field=models.CharField(blank=True, help_text='BOM item notes', max_length=500), + ), + ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 48e8dc7906..5f31e8a912 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -843,15 +843,19 @@ class Part(models.Model): 'Part', 'Description', 'Quantity', + 'Overage', + 'Reference', 'Note', ]) - for it in self.bom_items.all(): + for it in self.bom_items.all().order_by('id'): line = [] line.append(it.sub_part.full_name) line.append(it.sub_part.description) line.append(it.quantity) + line.append(it.overage) + line.append(it.reference) line.append(it.note) data.append(line) @@ -969,6 +973,7 @@ class BomItem(models.Model): part: Link to the parent part (the part that will be produced) sub_part: Link to the child part (the part that will be consumed) quantity: Number of 'sub_parts' consumed to produce one 'part' + reference: BOM reference field (e.g. part designators) overage: Estimated losses for a Build. Can be expressed as absolute value (e.g. '7') or a percentage (e.g. '2%') note: Note field for this BOM item """ @@ -1001,8 +1006,10 @@ class BomItem(models.Model): help_text='Estimated build wastage quantity (absolute or percentage)' ) + reference = models.CharField(max_length=500, blank=True, help_text='BOM item reference') + # Note attached to this BOM line item - note = models.CharField(max_length=100, blank=True, help_text='BOM item notes') + note = models.CharField(max_length=500, blank=True, help_text='BOM item notes') def clean(self): """ Check validity of the BomItem model. From 2831ac55c47a786c021963f8415793dc729ca00b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 27 Jun 2019 22:15:58 +1000 Subject: [PATCH 07/70] Link to download a BOM template file --- InvenTree/InvenTree/helpers.py | 28 +++++++++++++++++++ InvenTree/part/forms.py | 1 + InvenTree/part/templates/part/bom_upload.html | 13 +++++++++ InvenTree/part/urls.py | 3 ++ InvenTree/part/views.py | 14 +++++++++- 5 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 InvenTree/part/templates/part/bom_upload.html diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index ddb4e35fee..2c2f74fde6 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -6,6 +6,7 @@ import io import json import os.path from PIL import Image +import tablib from wsgiref.util import FileWrapper from django.http import StreamingHttpResponse @@ -91,6 +92,33 @@ def MakeBarcode(object_type, object_id, object_url, data={}): return json.dumps(data, sort_keys=True) +def IsValidSpreadsheetFormat(fmt): + """ Test if a file format specifier is in the valid list of spreadsheet file formats """ + + return fmt.lower() in ['csv', 'xls', 'xlsx', 'tsv'] + + +def MakeBomTemplate(fmt): + """ Generate a Bill of Materials upload template file (for user download) """ + + if not IsValidSpreadsheetFormat(fmt): + fmt = 'csv' + + fields = [ + 'Part', + 'Quantity', + 'Overage', + 'Reference', + 'Notes' + ] + + data = tablib.Dataset(headers=fields).export(fmt) + + filename = 'InvenTree_BOM_Template.' + fmt + + return DownloadFile(data, filename) + + def DownloadFile(data, filename, content_type='application/text'): """ Create a dynamic file for the user to download. diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index 4512859828..8c90de9dd2 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -8,6 +8,7 @@ from __future__ import unicode_literals from InvenTree.forms import HelperForm from django import forms +from django.core.validators import MinValueValidator from .models import Part, PartCategory, PartAttachment from .models import BomItem diff --git a/InvenTree/part/templates/part/bom_upload.html b/InvenTree/part/templates/part/bom_upload.html new file mode 100644 index 0000000000..b62cbc9c49 --- /dev/null +++ b/InvenTree/part/templates/part/bom_upload.html @@ -0,0 +1,13 @@ +{% extends "modal_form.html" %} + +{% block pre_form_content %} + +{{ block.super }} + +

Select a BOM file to upload for {{ part.name }} - {{ part.description }}.

+ +

The BOM file must contain the required named columns as provided in the BOM Upload Template

+ +

Supported file formats: .csv, .tsv, .xls, .xlsx

+ +{% endblock %} \ No newline at end of file diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index a8f5184972..bfb542cb3f 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -74,6 +74,9 @@ part_urls = [ # Create a new BOM item url(r'^bom/new/?', views.BomItemCreate.as_view(), name='bom-item-create'), + # Download a BOM upload template + url(r'^bom_template/?', views.BomUploadTemplate.as_view(), name='bom-upload-template'), + # Individual part url(r'^(?P\d+)/', include(part_detail_urls)), diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 08e3be79cb..1415547d86 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -12,6 +12,7 @@ from django.shortcuts import get_object_or_404 from django.utils.translation import gettext_lazy as _ from django.urls import reverse_lazy from django.views.generic import DetailView, ListView +from django.views.generic.edit import FormMixin from django.forms.models import model_to_dict from django.forms import HiddenInput, CheckboxInput @@ -26,7 +27,7 @@ from . import forms as part_forms from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView from InvenTree.views import QRCodeView -from InvenTree.helpers import DownloadFile, str2bool +from InvenTree.helpers import DownloadFile, str2bool, MakeBomTemplate from InvenTree.status_codes import OrderStatus @@ -722,6 +723,17 @@ class BomUpload(AjaxView): } return self.renderJsonResponse(request, form, data=data) +class BomUploadTemplate(AjaxView): + """ + Provide a BOM upload template file for download. + - Generates a template file in the provided format e.g. ?format=csv + """ + + def get(self, request, *args, **kwargs): + + export_format = request.GET.get('format', 'csv') + + return MakeBomTemplate(export_format) class BomDownload(AjaxView): From 65c74541248a0854b8c5c027ded01efb98a6798e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 27 Jun 2019 22:16:24 +1000 Subject: [PATCH 08/70] Require certain headers to be present in the file --- InvenTree/part/forms.py | 10 ++++++++ InvenTree/part/views.py | 52 ++++++++++++++++++++++++----------------- 2 files changed, 40 insertions(+), 22 deletions(-) diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index 8c90de9dd2..b0dcaabfb0 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -44,10 +44,20 @@ class BomImportForm(HelperForm): bom_file = forms.FileField(label='BOM file', required=True, help_text="Select BOM file to upload") + starting_row = forms.IntegerField( + required=True, + initial=2, + help_text='First row containing valid BOM data', + validators=[ + MinValueValidator(1) + ] + ) + class Meta: model = Part fields = [ 'bom_file', + 'starting_row', ] diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 1415547d86..3b676da4f2 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -620,7 +620,7 @@ class BomValidate(AjaxUpdateView): return self.renderJsonResponse(request, form, data, context=self.get_context()) -class BomUpload(AjaxView): +class BomUpload(AjaxView, FormMixin): """ View for uploading a BOM file, and handling BOM data importing. The BOM upload process is as follows: @@ -648,14 +648,15 @@ class BomUpload(AjaxView): """ ajax_form_title = 'Upload Bill of Materials' + ajax_template_name = 'part/bom_upload.html' + form_class = part_forms.BomImportForm - def get_form(self): - """ Return the correct form for the given step in the upload process """ + def get_context_data(self): + ctx = { + 'part': self.part + } - if 1 or self.request.method == 'GET': - form = part_forms.BomImportForm() - - return form + return ctx def get(self, request, *args, **kwargs): """ Perform the initial 'GET' request. @@ -665,15 +666,13 @@ class BomUpload(AjaxView): self.request = request # A valid Part object must be supplied. This is the 'parent' part for the BOM - part = get_object_or_404(Part, pk=self.kwargs['pk']) + self.part = get_object_or_404(Part, pk=self.kwargs['pk']) - form = self.get_form() + self.form = self.get_form() - return self.renderJsonResponse(request, form) + return self.renderJsonResponse(request, self.form) def handleBomFileUpload(self, bom_file): - - form = part_forms.BomImportForm() data = { # TODO - Validate the form if there isn't actually an error! @@ -689,20 +688,28 @@ class BomUpload(AjaxView): elif ext in ['.xls', '.xlsx']: raw_data = bom_file.read() else: - form.errors['bom_file'] = ['Unsupported file format: ' + ext] - return self.renderJsonResponse(self.request, form, data) + self.form.errors['bom_file'] = ['Unsupported file format: ' + ext] + return self.renderJsonResponse(self.request, self.form, data) # Now try to read the data try: bom_data = tablib.Dataset().load(raw_data) - print(bom_data) + + headers = [h.lower() for h in bom_data.headers] + + # Minimal set of required fields + for header in ['part', 'quantity', 'reference']: + if not header in headers: + self.form.errors['bom_file'] = [_("Missing required field '{f}'".format(f=header))] + break + except tablib.UnsupportedFormat: valid = False - form.errors['bom_file'] = [ + self.form.errors['bom_file'] = [ "Error reading '{f}' (Unsupported file format)".format(f=str(bom_file)), ] - return self.renderJsonResponse(self.request, form, data=data) + return self.renderJsonResponse(self.request, self.form, data=data) def post(self, request, *args, **kwargs): """ Perform the various 'POST' requests required. @@ -710,11 +717,12 @@ class BomUpload(AjaxView): self.request = request + self.part = get_object_or_404(Part, pk=self.kwargs['pk']) + self.form = self.get_form() + # Did the user POST a file named bom_file? bom_file = request.FILES.get('bom_file', None) - form = self.get_form() - if bom_file: return self.handleBomFileUpload(bom_file) @@ -722,7 +730,9 @@ class BomUpload(AjaxView): 'form_valid': False, } - return self.renderJsonResponse(request, form, data=data) + return self.renderJsonResponse(request, self.form, data=data) + + class BomUploadTemplate(AjaxView): """ Provide a BOM upload template file for download. @@ -742,8 +752,6 @@ class BomDownload(AjaxView): - File format should be passed as a query param e.g. ?format=csv """ - # TODO - This should no longer extend an AjaxView! - model = Part def get(self, request, *args, **kwargs): From 45d16f2c42a59631f032623f97f96bf73526b989 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 27 Jun 2019 22:46:11 +1000 Subject: [PATCH 09/70] Create new file bom.py for BOM helper functions - New class for managing BOM upload --- InvenTree/InvenTree/helpers.py | 27 ---------- InvenTree/part/bom.py | 92 ++++++++++++++++++++++++++++++++++ InvenTree/part/views.py | 38 ++++---------- 3 files changed, 102 insertions(+), 55 deletions(-) create mode 100644 InvenTree/part/bom.py diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index 2c2f74fde6..b37c73f028 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -92,33 +92,6 @@ def MakeBarcode(object_type, object_id, object_url, data={}): return json.dumps(data, sort_keys=True) -def IsValidSpreadsheetFormat(fmt): - """ Test if a file format specifier is in the valid list of spreadsheet file formats """ - - return fmt.lower() in ['csv', 'xls', 'xlsx', 'tsv'] - - -def MakeBomTemplate(fmt): - """ Generate a Bill of Materials upload template file (for user download) """ - - if not IsValidSpreadsheetFormat(fmt): - fmt = 'csv' - - fields = [ - 'Part', - 'Quantity', - 'Overage', - 'Reference', - 'Notes' - ] - - data = tablib.Dataset(headers=fields).export(fmt) - - filename = 'InvenTree_BOM_Template.' + fmt - - return DownloadFile(data, filename) - - def DownloadFile(data, filename, content_type='application/text'): """ Create a dynamic file for the user to download. diff --git a/InvenTree/part/bom.py b/InvenTree/part/bom.py new file mode 100644 index 0000000000..4f0560e259 --- /dev/null +++ b/InvenTree/part/bom.py @@ -0,0 +1,92 @@ +""" +Functionality for Bill of Material (BOM) management. +Primarily BOM upload tools. +""" + +from fuzzywuzzy import fuzz +import tablib +import os + +from django.utils.translation import gettext_lazy as _ +from django.core.exceptions import ValidationError +from .models import Part, BomItem + +from InvenTree.helpers import DownloadFile + + +def IsValidBOMFormat(fmt): + """ Test if a file format specifier is in the valid list of BOM file formats """ + + return fmt.strip().lower() in ['csv', 'xls', 'xlsx', 'tsv'] + + +def MakeBomTemplate(fmt): + """ Generate a Bill of Materials upload template file (for user download) """ + + fmt = fmt.strip().lower() + + if not IsValidBOMFormat(fmt): + fmt = 'csv' + + fields = [ + 'Part', + 'Quantity', + 'Overage', + 'Reference', + 'Notes' + ] + + data = tablib.Dataset(headers=fields).export(fmt) + + filename = 'InvenTree_BOM_Template.' + fmt + + return DownloadFile(data, filename) + + +class BomUploadManager: + """ Class for managing an uploaded BOM file """ + + # Fields which are absolutely necessary for valid upload + REQUIRED_HEADERS = [ + 'Part', + 'Quantity', + ] + + # Fields which are not necessary but can be populated + USEFUL_HEADERS = [ + 'REFERENCE', + 'OVERAGE', + 'NOTES' + ] + + def __init__(self, bom_file, starting_row=2): + """ Initialize the BomUpload class with a user-uploaded file object """ + self.starting_row = starting_row + print("Starting on row", starting_row) + self.process(bom_file) + + def process(self, bom_file): + """ Process a BOM file """ + + ext = os.path.splitext(bom_file.name)[-1].lower() + + if ext in ['.csv', '.tsv', ]: + # These file formats need string decoding + raw_data = bom_file.read().decode('utf-8') + elif ext in ['.xls', '.xlsx']: + raw_data = bom_file.read() + else: + raise ValidationError({'bom_file': _('Unsupported file format: {f}'.format(f=ext))}) + + try: + bom_data = tablib.Dataset().load(raw_data) + except tablib.UnsupportedFormat: + raise ValidationError({'bom_file': _('Error reading BOM file (invalid data)')}) + + # Now we have BOM data in memory! + + headers = [h.lower() for h in bom_data.headers] + + for header in self.REQUIRED_HEADERS: + if not header.lower() in headers: + raise ValidationError({'bom_file': _("Missing required field '{f}'".format(f=header))}) diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 3b676da4f2..11669b464b 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -8,6 +8,7 @@ from __future__ import unicode_literals import tablib import os +from django.core.exceptions import ValidationError from django.shortcuts import get_object_or_404 from django.utils.translation import gettext_lazy as _ from django.urls import reverse_lazy @@ -23,11 +24,12 @@ from .models import match_part_names from company.models import SupplierPart from . import forms as part_forms +from .bom import MakeBomTemplate, BomUploadManager from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView from InvenTree.views import QRCodeView -from InvenTree.helpers import DownloadFile, str2bool, MakeBomTemplate +from InvenTree.helpers import DownloadFile, str2bool from InvenTree.status_codes import OrderStatus @@ -679,35 +681,15 @@ class BomUpload(AjaxView, FormMixin): 'form_valid': False } - # Extract the file format data - ext = os.path.splitext(bom_file.name)[-1].lower() - - if ext in ['.csv', '.tsv', ]: - # These file formats need string decoding - raw_data = bom_file.read().decode('utf-8') - elif ext in ['.xls', '.xlsx']: - raw_data = bom_file.read() - else: - self.form.errors['bom_file'] = ['Unsupported file format: ' + ext] - return self.renderJsonResponse(self.request, self.form, data) - - # Now try to read the data + # Create a BomUploadManager object - will perform initial data validation + # (and raise a ValidationError if there is something wrong with the file) try: - bom_data = tablib.Dataset().load(raw_data) + manager = BomUploadManager(bom_file, self.form['starting_row'].value()) + except ValidationError as e: + errors = e.error_dict - headers = [h.lower() for h in bom_data.headers] - - # Minimal set of required fields - for header in ['part', 'quantity', 'reference']: - if not header in headers: - self.form.errors['bom_file'] = [_("Missing required field '{f}'".format(f=header))] - break - - except tablib.UnsupportedFormat: - valid = False - self.form.errors['bom_file'] = [ - "Error reading '{f}' (Unsupported file format)".format(f=str(bom_file)), - ] + for k,v in errors.items(): + self.form.errors[k] = v return self.renderJsonResponse(self.request, self.form, data=data) From 9813205419a8c2eb3ccbd630c7c17bfae11758d3 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 27 Jun 2019 22:57:55 +1000 Subject: [PATCH 10/70] Perform 'matching' on imported field names --- InvenTree/part/bom.py | 58 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/InvenTree/part/bom.py b/InvenTree/part/bom.py index 4f0560e259..19bdf8466b 100644 --- a/InvenTree/part/bom.py +++ b/InvenTree/part/bom.py @@ -54,9 +54,9 @@ class BomUploadManager: # Fields which are not necessary but can be populated USEFUL_HEADERS = [ - 'REFERENCE', - 'OVERAGE', - 'NOTES' + 'Reference', + 'Overage', + 'Notes' ] def __init__(self, bom_file, starting_row=2): @@ -79,14 +79,60 @@ class BomUploadManager: raise ValidationError({'bom_file': _('Unsupported file format: {f}'.format(f=ext))}) try: - bom_data = tablib.Dataset().load(raw_data) + self.data = tablib.Dataset().load(raw_data) except tablib.UnsupportedFormat: raise ValidationError({'bom_file': _('Error reading BOM file (invalid data)')}) # Now we have BOM data in memory! - headers = [h.lower() for h in bom_data.headers] + self.header_map = {} for header in self.REQUIRED_HEADERS: - if not header.lower() in headers: + match = self.get_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.get_header(header) + + self.header_map[header] = match + + for k,v in self.header_map.items(): + print(k, '->', v) + + def get_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. + """ + + headers = self.data.headers + + # 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 + + # Finally, look for a close match using fuzzy matching + + matches = [] + + for header in headers: + + ratio = fuzz.partial_ratio(header, header_name) + if ratio > threshold: + matches.append({'header': header, '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 From c4944268f33b7112f0a70be7f44fda1ad2344fb2 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 27 Jun 2019 23:09:06 +1000 Subject: [PATCH 11/70] Count the number of rows in the BOM file --- InvenTree/part/bom.py | 47 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/InvenTree/part/bom.py b/InvenTree/part/bom.py index 19bdf8466b..594275b670 100644 --- a/InvenTree/part/bom.py +++ b/InvenTree/part/bom.py @@ -61,13 +61,21 @@ class BomUploadManager: def __init__(self, bom_file, starting_row=2): """ Initialize the BomUpload class with a user-uploaded file object """ - self.starting_row = starting_row - print("Starting on row", starting_row) + try: + start = int(starting_row) - 1 + if start < 0: + start = 0 + self.starting_row = start + except ValueError: + self.starting_row = 1 + self.process(bom_file) def process(self, bom_file): """ Process a BOM file """ + self.data = None + ext = os.path.splitext(bom_file.name)[-1].lower() if ext in ['.csv', '.tsv', ]: @@ -88,21 +96,30 @@ class BomUploadManager: self.header_map = {} for header in self.REQUIRED_HEADERS: - match = self.get_header(header) + 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.get_header(header) + match = self.extract_header(header) self.header_map[header] = match + # Now we have mapped data to each header for k,v in self.header_map.items(): print(k, '->', v) - def get_header(self, header_name, threshold=80): + 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. """ @@ -136,3 +153,23 @@ class BomUploadManager: return matches[0]['header'] return None + + def row_count(self): + """ Return the number of rows in the file. + Ignored the top rows as indicated by 'starting row' + """ + + if self.data is None: + return 0 + + return len(self.data) - self.starting_row + + def get_row(self, index): + """ Retrieve a dict object representing the data row at a particular offset """ + + index += self.starting_row + + if self.data is None or index >= len(self.data): + return None + + return self.data.dict[index] From 6af51c5b3500a940fa8c6e323857ffa64e1ca631 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 27 Jun 2019 23:11:54 +1000 Subject: [PATCH 12/70] Limit the number of lines returned in row_count --- InvenTree/part/bom.py | 5 ++++- InvenTree/part/templates/part/bom_upload.html | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/InvenTree/part/bom.py b/InvenTree/part/bom.py index 594275b670..6841de2146 100644 --- a/InvenTree/part/bom.py +++ b/InvenTree/part/bom.py @@ -162,7 +162,10 @@ class BomUploadManager: if self.data is None: return 0 - return len(self.data) - self.starting_row + # Limit the number of BOM lines to be sensible + count = min(len(self.data) - self.starting_row, 1000) + + return count def get_row(self, index): """ Retrieve a dict object representing the data row at a particular offset """ diff --git a/InvenTree/part/templates/part/bom_upload.html b/InvenTree/part/templates/part/bom_upload.html index b62cbc9c49..50ff5cdf87 100644 --- a/InvenTree/part/templates/part/bom_upload.html +++ b/InvenTree/part/templates/part/bom_upload.html @@ -8,6 +8,10 @@

The BOM file must contain the required named columns as provided in the BOM Upload Template

-

Supported file formats: .csv, .tsv, .xls, .xlsx

+Notes: +
    +
  • Supported file formats: .csv, .tsv, .xls, .xlsx
  • +
  • Maximum of 1000 lines per BOM
  • +
{% endblock %} \ No newline at end of file From a9396f4c74d59f711bed33e2739b6124e31b1c0f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 27 Jun 2019 23:13:12 +1000 Subject: [PATCH 13/70] Fix uggo buttons --- InvenTree/part/templates/part/bom.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/InvenTree/part/templates/part/bom.html b/InvenTree/part/templates/part/bom.html index 04101ab995..d077bc9d9a 100644 --- a/InvenTree/part/templates/part/bom.html +++ b/InvenTree/part/templates/part/bom.html @@ -33,10 +33,10 @@
{% if editing_enabled %} - - - - + + + + {% else %} {% if part.is_bom_valid == False %} From 4648db6ce5ab918fbfd151e5f699843bbb3c017e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 27 Jun 2019 23:49:01 +1000 Subject: [PATCH 14/70] Pass file data through to the next form page --- InvenTree/part/bom.py | 49 ++++++++---- InvenTree/part/forms.py | 24 +++--- .../part/bom_upload/select_fields.html | 38 +++++++++ .../select_file.html} | 10 ++- .../part/templatetags/inventree_extras.py | 5 ++ InvenTree/part/views.py | 77 +++++++++++++++---- 6 files changed, 161 insertions(+), 42 deletions(-) create mode 100644 InvenTree/part/templates/part/bom_upload/select_fields.html rename InvenTree/part/templates/part/{bom_upload.html => bom_upload/select_file.html} (57%) diff --git a/InvenTree/part/bom.py b/InvenTree/part/bom.py index 6841de2146..e2d870805a 100644 --- a/InvenTree/part/bom.py +++ b/InvenTree/part/bom.py @@ -59,15 +59,8 @@ class BomUploadManager: 'Notes' ] - def __init__(self, bom_file, starting_row=2): + def __init__(self, bom_file): """ Initialize the BomUpload class with a user-uploaded file object """ - try: - start = int(starting_row) - 1 - if start < 0: - start = 0 - self.starting_row = start - except ValueError: - self.starting_row = 1 self.process(bom_file) @@ -154,6 +147,20 @@ class BomUploadManager: return None + + def get_headers(self): + """ Return a list of headers for the thingy """ + headers = [] + + 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. Ignored the top rows as indicated by 'starting row' @@ -162,16 +169,30 @@ class BomUploadManager: if self.data is None: return 0 - # Limit the number of BOM lines to be sensible - count = min(len(self.data) - self.starting_row, 1000) + return len(self.data) - return count + def rows(self): + """ Return a list of all rows """ + rows = [] - def get_row(self, index): + for i in range(self.row_count()): + row = self.get_row_data(i) + + if row: + 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 """ - index += self.starting_row - if self.data is None or index >= len(self.data): return None diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index b0dcaabfb0..cb6ae9c70b 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -39,25 +39,29 @@ class BomValidateForm(HelperForm): ] -class BomImportForm(HelperForm): +class BomUploadSelectFile(HelperForm): """ Form for importing a BOM. Provides a file input box for upload """ bom_file = forms.FileField(label='BOM file', required=True, help_text="Select BOM file to upload") - starting_row = forms.IntegerField( - required=True, - initial=2, - help_text='First row containing valid BOM data', - validators=[ - MinValueValidator(1) - ] - ) - class Meta: model = Part fields = [ 'bom_file', + ] + + +class BomUploadSelectFields(HelperForm): + """ Form for selecting BOM fields """ + + starting_row = forms.IntegerField(required=True, initial=2, help_text='Index of starting row', validators=[MinValueValidator(1)]) + row_count = forms.IntegerField(required=True, help_text='Number of rows to process', validators=[MinValueValidator(0)]) + + class Meta: + model = Part + fields = [ 'starting_row', + 'row_count', ] diff --git a/InvenTree/part/templates/part/bom_upload/select_fields.html b/InvenTree/part/templates/part/bom_upload/select_fields.html new file mode 100644 index 0000000000..0656a957a5 --- /dev/null +++ b/InvenTree/part/templates/part/bom_upload/select_fields.html @@ -0,0 +1,38 @@ +{% extends "modal_form.html" %} +{% load inventree_extras %} + +{% block form %} + +

Step 2 of 3 - Select BOM Fields

+ +
+ {% csrf_token %} + {% load crispy_forms_tags %} + + {% crispy form %} + + + + + + + + + + {% for row in bom.rows %} + + {% for item in row %} + + {% endfor %} + + {% endfor %} + +
+ {{ item }} +
+ +
+ +BOM Rows: {{ bom.row_count }} + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/bom_upload.html b/InvenTree/part/templates/part/bom_upload/select_file.html similarity index 57% rename from InvenTree/part/templates/part/bom_upload.html rename to InvenTree/part/templates/part/bom_upload/select_file.html index 50ff5cdf87..68380dbfd5 100644 --- a/InvenTree/part/templates/part/bom_upload.html +++ b/InvenTree/part/templates/part/bom_upload/select_file.html @@ -2,9 +2,13 @@ {% block pre_form_content %} +

Step 1 of 3 - Select BOM File

+ {{ block.super }} -

Select a BOM file to upload for {{ part.name }} - {{ part.description }}.

+

Select a BOM file to upload for:
+ {{ part.name }} - {{ part.description }} +

The BOM file must contain the required named columns as provided in the BOM Upload Template

@@ -14,4 +18,8 @@
  • Maximum of 1000 lines per BOM
  • +{% endblock %} + +{% block form_data %} + {% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py index b17c5bf9e8..493a1bb740 100644 --- a/InvenTree/part/templatetags/inventree_extras.py +++ b/InvenTree/part/templatetags/inventree_extras.py @@ -8,6 +8,11 @@ from InvenTree import version register = template.Library() +@register.simple_tag() +def inrange(n, *args, **kwargs): + """ Return range(n) for iterating through a numeric quantity """ + return range(n) + @register.simple_tag() def multiply(x, y, *args, **kwargs): """ Multiply two numbers together """ diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 11669b464b..f5d689875a 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -650,8 +650,17 @@ class BomUpload(AjaxView, FormMixin): """ ajax_form_title = 'Upload Bill of Materials' - ajax_template_name = 'part/bom_upload.html' - form_class = part_forms.BomImportForm + ajax_template_name = 'part/bom_upload/select_file.html' + + def get_form_class(self): + + form_step = self.request.POST.get('form_step', None) + + if form_step == 'select_fields': + return part_forms.BomUploadSelectFields + else: + # Default form is the starting point + return part_forms.BomUploadSelectFile def get_context_data(self): ctx = { @@ -674,24 +683,54 @@ class BomUpload(AjaxView, FormMixin): return self.renderJsonResponse(request, self.form) - def handleBomFileUpload(self, bom_file): - + def handleBomFileUpload(self): + + bom_file = self.request.FILES.get('bom_file', None) + + manager = None + bom_file_valid = False + + if bom_file is None: + self.form.errors['bom_file'] = [_('No BOM file provided')] + else: + # Create a BomUploadManager object - will perform initial data validation + # (and raise a ValidationError if there is something wrong with the file) + try: + manager = BomUploadManager(bom_file) + bom_file_valid = True + except ValidationError as e: + errors = e.error_dict + + for k,v in errors.items(): + self.form.errors[k] = v + data = { - # TODO - Validate the form if there isn't actually an error! 'form_valid': False } - # Create a BomUploadManager object - will perform initial data validation - # (and raise a ValidationError if there is something wrong with the file) - try: - manager = BomUploadManager(bom_file, self.form['starting_row'].value()) - except ValidationError as e: - errors = e.error_dict + ctx = {} - for k,v in errors.items(): - self.form.errors[k] = v + if bom_file_valid: + # 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 + else: + form = self.form - return self.renderJsonResponse(self.request, self.form, data=data) + return self.renderJsonResponse(self.request, form, data=data, context=ctx) + + def handleFieldSelection(self): + + data = { + 'form_valid': False, + } + + self.ajax_template_name = 'part/bom_upload/select_fields.html' + + ctx = {} + + return self.renderJsonResponse(self.request, form=self.get_form(), data=data, context=ctx) def post(self, request, *args, **kwargs): """ Perform the various 'POST' requests required. @@ -703,10 +742,14 @@ class BomUpload(AjaxView, FormMixin): self.form = self.get_form() # Did the user POST a file named bom_file? - bom_file = request.FILES.get('bom_file', None) + - if bom_file: - return self.handleBomFileUpload(bom_file) + form_step = request.POST.get('form_step', None) + + if form_step == 'select_file': + return self.handleBomFileUpload() + elif form_step == 'select_fields': + return self.handleFieldSelection() data = { 'form_valid': False, From 808d332bdaa2b8e150436df6aa63022fd83d1d3f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 27 Jun 2019 23:57:21 +1000 Subject: [PATCH 15/70] Show BomItem reference field in BOM table --- InvenTree/part/forms.py | 1 + InvenTree/part/serializers.py | 1 + InvenTree/static/script/inventree/bom.js | 50 +++++++++++++----------- 3 files changed, 30 insertions(+), 22 deletions(-) diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index cb6ae9c70b..09e9c75f3b 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -136,6 +136,7 @@ class EditBomItemForm(HelperForm): 'part', 'sub_part', 'quantity', + 'reference', 'overage', 'note' ] diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index eaea7ecebc..b3d8c17b12 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -166,6 +166,7 @@ class BomItemSerializer(InvenTreeModelSerializer): 'sub_part', 'sub_part_detail', 'quantity', + 'reference', 'price_range', 'overage', 'note', diff --git a/InvenTree/static/script/inventree/bom.js b/InvenTree/static/script/inventree/bom.js index 0f3134c193..47a3151f43 100644 --- a/InvenTree/static/script/inventree/bom.js +++ b/InvenTree/static/script/inventree/bom.js @@ -63,33 +63,39 @@ function loadBomTable(table, options) { } ); + // Part reference + cols.push({ + field: 'reference', + title: 'Reference', + searchable: true, + sortable: true, + }); + // Part quantity - cols.push( - { - field: 'quantity', - title: 'Required', - searchable: false, - sortable: true, - formatter: function(value, row, index, field) { - var text = value; + cols.push({ + field: 'quantity', + title: 'Quantity', + searchable: false, + sortable: true, + formatter: function(value, row, index, field) { + var text = value; - if (row.overage) { - text += " (+" + row.overage + ") "; - } + if (row.overage) { + text += " (+" + row.overage + ") "; + } - return text; - }, - footerFormatter: function(data) { - var quantity = 0; + return text; + }, + footerFormatter: function(data) { + var quantity = 0; - data.forEach(function(item) { - quantity += item.quantity; - }); + data.forEach(function(item) { + quantity += item.quantity; + }); - return quantity; - }, - } - ); + return quantity; + }, + }); if (!options.editable) { cols.push( From 872329c340e7f6a485fb20a6eec899f3999f218c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 28 Jun 2019 00:10:24 +1000 Subject: [PATCH 16/70] Fix BOM validation button --- InvenTree/part/templates/part/bom.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/part/templates/part/bom.html b/InvenTree/part/templates/part/bom.html index d077bc9d9a..1605deda68 100644 --- a/InvenTree/part/templates/part/bom.html +++ b/InvenTree/part/templates/part/bom.html @@ -40,7 +40,7 @@ {% else %} {% if part.is_bom_valid == False %} - + {% endif %}
    diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 99ea6680e3..94d433a30a 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -1042,6 +1042,9 @@ class BomItemCreate(AjaxCreateView): query = query.exclude(id__in=[item.id for item in part.required_parts()]) form.fields['sub_part'].queryset = query + + form.fields['part'].widget = HiddenInput() + except Part.DoesNotExist: pass @@ -1076,40 +1079,6 @@ class BomItemEdit(AjaxUpdateView): ajax_template_name = 'modal_form.html' ajax_form_title = 'Edit BOM item' - def get_form(self): - """ Override get_form() method to reduce Part selection options. - - - Do not allow part to be added to its own BOM - - Remove any Part items that are already in the BOM - """ - - form = super(AjaxCreateView, self).get_form() - - part_id = form['part'].value() - - try: - part = Part.objects.get(id=part_id) - - # Only allow active parts to be selected - query = form.fields['part'].queryset.filter(active=True) - form.fields['part'].queryset = query - - # Don't allow selection of sub_part objects which are already added to the Bom! - query = form.fields['sub_part'].queryset - - # Don't allow a part to be added to its own BOM - query = query.exclude(id=part.id) - query = query.filter(active=True) - - # Eliminate any options that are already in the BOM! - query = query.exclude(id__in=[item.id for item in part.required_parts()]) - - form.fields['sub_part'].queryset = query - except Part.DoesNotExist: - pass - - return form - class BomItemDelete(AjaxDeleteView): """ Delete view for removing BomItem """ From fb96651c1537116e4c4c77a35b519c319a105d8f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 28 Jun 2019 19:40:27 +1000 Subject: [PATCH 22/70] Render column selection options - Guess which header is which --- InvenTree/part/bom.py | 74 +++++++------------ .../part/bom_upload/select_fields.html | 15 +++- .../part/templatetags/inventree_extras.py | 6 ++ InvenTree/part/views.py | 26 ++++++- 4 files changed, 69 insertions(+), 52 deletions(-) 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 %} - {% for item in row %} + {% for item in row.data %} {% endfor %} diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index ad55f75f3d..6858e3e179 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -721,8 +721,6 @@ class BomUpload(AjaxView, FormMixin): form = part_forms.BomUploadSelectFields self.ajax_template_name = 'part/bom_upload/select_fields.html' - # Try to guess at the - # Provide context to the next form ctx = { 'req_cols': BomUploadManager.HEADERS, From 857a214e7df3b90d4c4929f179622b0856ea4d67 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 28 Jun 2019 19:58:56 +1000 Subject: [PATCH 24/70] Pass the form field data back to the server --- .../part/bom_upload/select_fields.html | 4 +- InvenTree/part/views.py | 43 +++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/InvenTree/part/templates/part/bom_upload/select_fields.html b/InvenTree/part/templates/part/bom_upload/select_fields.html index db09b2a3fb..d06846b33d 100644 --- a/InvenTree/part/templates/part/bom_upload/select_fields.html +++ b/InvenTree/part/templates/part/bom_upload/select_fields.html @@ -19,13 +19,13 @@ {% for col in bom_cols %} {% endfor %} diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 6858e3e179..abd787bc5c 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -744,6 +744,49 @@ class BomUpload(AjaxView, FormMixin): self.ajax_template_name = 'part/bom_upload/select_fields.html' + # Map the columns + column_names = {} + column_selections = {} + + row_data = {} + + for item in self.request.POST: + + print(item) + + value = self.request.POST[item] + + # Extract the column names + if item.startswith('col_name_'): + col_id = item.replace('col_name_', '') + col_name = value + + column_names[col_id] = col_name + + # Extract the column selections + if item.startswith('col_select_'): + + col_id = item.replace('col_select_', '') + col_name = value + + column_selections[col_id] = value + + # Extract the row data + if item.startswith('row_'): + # Item should be of the format row__col_ + s = item.split('_') + + if len(s) < 4: + continue + + row_id = s[1] + col_id = s[3] + + if not row_id in row_data: + row_data[row_id] = {} + + row_data[row_id][col_id] = value + ctx = { # The headers that we know about 'known_headers': BomUploadManager.HEADERS, From fd8ed44833fbc873b09e2dd056f3924b1318d688 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 28 Jun 2019 20:16:17 +1000 Subject: [PATCH 25/70] Detect duplicate columns, and missing columns --- InvenTree/part/bom.py | 9 +++- .../part/bom_upload/select_fields.html | 14 ++++++ InvenTree/part/views.py | 46 +++++++++++++++---- 3 files changed, 59 insertions(+), 10 deletions(-) diff --git a/InvenTree/part/bom.py b/InvenTree/part/bom.py index fe973efaf0..b0ba1a5dae 100644 --- a/InvenTree/part/bom.py +++ b/InvenTree/part/bom.py @@ -46,14 +46,19 @@ class BomUploadManager: """ Class for managing an uploaded BOM file """ # Fields which are absolutely necessary for valid upload - HEADERS = [ + REQUIRED_HEADERS = [ 'Part', - 'Quantity', + 'Quantity' + ] + + OPTIONAL_HEADERS = [ 'Reference', 'Overage', 'Notes' ] + HEADERS = REQUIRED_HEADERS + OPTIONAL_HEADERS + def __init__(self, bom_file): """ Initialize the BomUpload class with a user-uploaded file object """ diff --git a/InvenTree/part/templates/part/bom_upload/select_fields.html b/InvenTree/part/templates/part/bom_upload/select_fields.html index d06846b33d..6bb6223dba 100644 --- a/InvenTree/part/templates/part/bom_upload/select_fields.html +++ b/InvenTree/part/templates/part/bom_upload/select_fields.html @@ -5,6 +5,17 @@

    Step 2 of 3 - Select BOM Fields

    +{% if missing and missing|length > 0 %} + +{% endif %} +
    {% csrf_token %} {% load crispy_forms_tags %} @@ -25,6 +36,9 @@ {% endfor %} + {% if col.duplicate %} +

    Duplicate column selection

    + {% endif %} {{ col.name }} diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index abd787bc5c..1c90a54752 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -752,13 +752,11 @@ class BomUpload(AjaxView, FormMixin): for item in self.request.POST: - print(item) - value = self.request.POST[item] # Extract the column names if item.startswith('col_name_'): - col_id = item.replace('col_name_', '') + col_id = int(item.replace('col_name_', '')) col_name = value column_names[col_id] = col_name @@ -766,7 +764,7 @@ class BomUpload(AjaxView, FormMixin): # Extract the column selections if item.startswith('col_select_'): - col_id = item.replace('col_select_', '') + col_id = int(item.replace('col_select_', '')) col_name = value column_selections[col_id] = value @@ -779,17 +777,49 @@ class BomUpload(AjaxView, FormMixin): if len(s) < 4: continue - row_id = s[1] - col_id = s[3] + row_id = int(s[1]) + col_id = int(s[3]) if not row_id in row_data: row_data[row_id] = {} row_data[row_id][col_id] = value - + + col_ids = sorted(column_names.keys()) + + headers = [] + + for col in col_ids: + if col not in column_selections: + continue + + header = ({ + 'name': column_names[col], + 'guess': column_selections[col] + }) + + # Duplicate guess? + guess = column_selections[col] + + if guess: + n = list(column_selections.values()).count(column_selections[col]) + if n > 1: + header['duplicate'] = True + + headers.append(header) + + # Are there any missing columns? + missing = [] + + for col in BomUploadManager.REQUIRED_HEADERS: + if not col in column_selections.values(): + missing.append(col) + ctx = { # The headers that we know about - 'known_headers': BomUploadManager.HEADERS, + 'req_cols': BomUploadManager.HEADERS, + 'bom_cols': headers, + 'missing': missing, } return self.renderJsonResponse(self.request, form=self.get_form(), data=data, context=ctx) From 54762713f3ecb8adf238e3febd704521bf967289 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 28 Jun 2019 20:21:21 +1000 Subject: [PATCH 26/70] Pass row data back through again --- InvenTree/part/views.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 1c90a54752..9b8ca5774d 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -815,13 +815,28 @@ class BomUpload(AjaxView, FormMixin): if not col in column_selections.values(): missing.append(col) + # Re-construct the data table + rows = [] + + for row_idx in sorted(row_data.keys()): + row = row_data[row_idx] + items = [] + for col_idx in sorted(row.keys()): + value = row[col_idx] + items.append(value) + + rows.append({'index': row_idx, 'data': items}) + ctx = { # The headers that we know about 'req_cols': BomUploadManager.HEADERS, 'bom_cols': headers, 'missing': missing, + 'bom_rows': rows, } + print(ctx) + return self.renderJsonResponse(self.request, form=self.get_form(), data=data, context=ctx) def post(self, request, *args, **kwargs): From 3c2f3c2c2cab645d36ebe9d71e6b983ccb8e6fad Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 28 Jun 2019 20:24:18 +1000 Subject: [PATCH 27/70] Add option to delete columns --- .../part/templates/part/bom_upload/select_fields.html | 1 + InvenTree/part/views.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/InvenTree/part/templates/part/bom_upload/select_fields.html b/InvenTree/part/templates/part/bom_upload/select_fields.html index 6bb6223dba..e74c49691a 100644 --- a/InvenTree/part/templates/part/bom_upload/select_fields.html +++ b/InvenTree/part/templates/part/bom_upload/select_fields.html @@ -35,6 +35,7 @@ {% for req in req_cols %} {% endfor %} + {% if col.duplicate %}

    Duplicate column selection

    diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 9b8ca5774d..94fc00b30a 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -767,6 +767,9 @@ class BomUpload(AjaxView, FormMixin): col_id = int(item.replace('col_select_', '')) col_name = value + if value.lower() == 'delete': + continue + column_selections[col_id] = value # Extract the row data @@ -822,6 +825,10 @@ class BomUpload(AjaxView, FormMixin): row = row_data[row_idx] items = [] for col_idx in sorted(row.keys()): + + if not col_idx in column_selections.keys(): + continue + value = row[col_idx] items.append(value) From cfbfc6e258dcc8093e0a89e378d027e9268e9f14 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 28 Jun 2019 20:41:45 +1000 Subject: [PATCH 28/70] Delete columns in-place using javascript --- .../part/bom_upload/select_fields.html | 19 +++++++++++-------- InvenTree/part/views.py | 3 --- InvenTree/static/script/inventree/bom.js | 19 +++++++++++++++++++ 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/InvenTree/part/templates/part/bom_upload/select_fields.html b/InvenTree/part/templates/part/bom_upload/select_fields.html index e74c49691a..1175ed353b 100644 --- a/InvenTree/part/templates/part/bom_upload/select_fields.html +++ b/InvenTree/part/templates/part/bom_upload/select_fields.html @@ -7,7 +7,8 @@ {% if missing and missing|length > 0 %}
    {% for col in bom_cols %} {% endfor %} diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 94fc00b30a..1763ce918e 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -767,9 +767,6 @@ class BomUpload(AjaxView, FormMixin): col_id = int(item.replace('col_select_', '')) col_name = value - if value.lower() == 'delete': - continue - column_selections[col_id] = value # Extract the row data diff --git a/InvenTree/static/script/inventree/bom.js b/InvenTree/static/script/inventree/bom.js index 47a3151f43..581fb8d4a3 100644 --- a/InvenTree/static/script/inventree/bom.js +++ b/InvenTree/static/script/inventree/bom.js @@ -12,6 +12,25 @@ function reloadBomTable(table, options) { } +function removeColFromBomWizard(e) { + /* Remove a column from BOM upload wizard + */ + + e = e || window.event; + + var src = e.target || e.srcElement; + + // Which column was clicked? + var col = $(src).closest('th').index(); + + var table = $(src).closest('table'); + + table.find('tr').each(function() { + this.removeChild(this.cells[col]); + }); +} + + function loadBomTable(table, options) { /* Load a BOM table with some configurable options. * From ad27d912e1a12ca8a323f12ff1744caaea4bac98 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 28 Jun 2019 20:48:23 +1000 Subject: [PATCH 29/70] Add some optional upload fields (These will come in handy later) --- InvenTree/part/bom.py | 6 ++++++ InvenTree/part/templates/part/bom_upload/select_fields.html | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/InvenTree/part/bom.py b/InvenTree/part/bom.py index b0ba1a5dae..3b6a7fe928 100644 --- a/InvenTree/part/bom.py +++ b/InvenTree/part/bom.py @@ -51,8 +51,14 @@ class BomUploadManager: 'Quantity' ] + # Fields which would be helpful but are not required OPTIONAL_HEADERS = [ 'Reference', + 'Description', + 'Category', + 'Supplier', + 'Manufacturer', + 'MPN', 'Overage', 'Notes' ] diff --git a/InvenTree/part/templates/part/bom_upload/select_fields.html b/InvenTree/part/templates/part/bom_upload/select_fields.html index 1175ed353b..f38a9dae34 100644 --- a/InvenTree/part/templates/part/bom_upload/select_fields.html +++ b/InvenTree/part/templates/part/bom_upload/select_fields.html @@ -34,7 +34,7 @@ {{ col.name }} {% for col in bom_cols %} + {% endfor %} + + + + + {% for col in bom_cols %} + {% endfor %} - - {% for row in bom_rows %} From a23595c28db179c764ba43428b2a1f930c276955 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 29 Jun 2019 19:56:04 +1000 Subject: [PATCH 31/70] Improve data importing - Automatically prune empty rows - prevent automatic conversion of integers to floats --- InvenTree/part/bom.py | 27 ++++++++++++++++--- .../part/bom_upload/select_fields.html | 1 + InvenTree/part/views.py | 9 +++---- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/InvenTree/part/bom.py b/InvenTree/part/bom.py index 3b6a7fe928..70cea0601b 100644 --- a/InvenTree/part/bom.py +++ b/InvenTree/part/bom.py @@ -121,7 +121,6 @@ class BomUploadManager: return matches[0]['header'] return None - def get_headers(self): """ Return a list of headers for the thingy """ @@ -157,13 +156,33 @@ class BomUploadManager: 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)): + print("converting", item, "to", int(item)) + data[idx] = int(item) + except ValueError: + pass + + if empty: + print("Empty - continuing") + continue + row = { - 'data': self.get_row_data(i), + 'data': data, 'index': i } - if row: - rows.append(row) + rows.append(row) return rows diff --git a/InvenTree/part/templates/part/bom_upload/select_fields.html b/InvenTree/part/templates/part/bom_upload/select_fields.html index ee4a61fc0f..ad90530ad8 100644 --- a/InvenTree/part/templates/part/bom_upload/select_fields.html +++ b/InvenTree/part/templates/part/bom_upload/select_fields.html @@ -44,6 +44,7 @@ + {% for col in bom_cols %} - {% for col in bom_cols %} + {% for col in bom_columns %} - {% for col in bom_cols %} + {% for col in bom_columns %}
    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) From 58336482fe481f889bd40cb0ab233834ac3ac9a5 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 28 Jun 2019 19:48:22 +1000 Subject: [PATCH 23/70] POST the data back again --- InvenTree/part/bom.py | 8 ++++++-- .../part/templates/part/bom_upload/select_fields.html | 4 +++- InvenTree/part/views.py | 2 -- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/InvenTree/part/bom.py b/InvenTree/part/bom.py index 5108672e66..fe973efaf0 100644 --- a/InvenTree/part/bom.py +++ b/InvenTree/part/bom.py @@ -79,7 +79,7 @@ class BomUploadManager: except tablib.UnsupportedFormat: raise ValidationError({'bom_file': _('Error reading BOM file (invalid data)')}) - def guess_headers(self, header, threshold=80): + def guess_header(self, header, threshold=80): """ Try to match a header (from the file) to a list of known headers Args: @@ -145,7 +145,11 @@ class BomUploadManager: rows = [] for i in range(self.row_count()): - row = self.get_row_data(i) + + row = { + 'data': self.get_row_data(i), + 'index': i + } if row: rows.append(row) diff --git a/InvenTree/part/templates/part/bom_upload/select_fields.html b/InvenTree/part/templates/part/bom_upload/select_fields.html index 709cf7a18d..db09b2a3fb 100644 --- a/InvenTree/part/templates/part/bom_upload/select_fields.html +++ b/InvenTree/part/templates/part/bom_upload/select_fields.html @@ -25,6 +25,7 @@ {% endfor %} + {{ col.name }} {% endfor %} @@ -34,8 +35,9 @@ {% for row in bom_rows %}
    {% add forloop.counter 1 %} + {{ item }} Row - {% for req in req_cols %} {% endfor %} - + {{ col.name }} Row + + {{ col.name }} + {% if col.duplicate %}

    Duplicate column selection

    {% endif %} - - {{ col.name }}
    Row - - {{ col.name }} - +
    + + {{ col.name }} + +
    +
    {% add forloop.counter 1 %}
    + + {% crispy form %] + + + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index bfb542cb3f..22cff6c83d 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -22,12 +22,13 @@ part_detail_urls = [ url(r'^edit/?', views.PartEdit.as_view(), name='part-edit'), url(r'^delete/?', views.PartDelete.as_view(), name='part-delete'), url(r'^bom-export/?', views.BomDownload.as_view(), name='bom-export'), - url(r'^bom-import/?', views.BomUpload.as_view(), name='bom-import'), url(r'^validate-bom/', views.BomValidate.as_view(), name='bom-validate'), url(r'^duplicate/', views.PartDuplicate.as_view(), name='part-duplicate'), url(r'^make-variant/', views.MakePartVariant.as_view(), name='make-part-variant'), url(r'^pricing/', views.PartPricing.as_view(), name='part-pricing'), + url(r'^bom-upload/?', views.BomUpload.as_view(template_name='part/bom_upload/upload_file.html'), name='upload-bom'), + url(r'^variants/?', views.PartDetail.as_view(template_name='part/variants.html'), name='part-variants'), url(r'^stock/?', views.PartDetail.as_view(template_name='part/stock.html'), name='part-stock'), url(r'^allocation/?', views.PartDetail.as_view(template_name='part/allocation.html'), name='part-allocation'), From 4f5b87dd38a97be11126d25ea1fa6e5da8fd4c5d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 2 Jul 2019 18:44:49 +1000 Subject: [PATCH 34/70] Comment out postgres requirement --- InvenTree/InvenTree/settings.py | 17 +++++++++-------- requirements.txt | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index bb50ed04f4..83549d70e2 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -153,14 +153,15 @@ DATABASES = { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'inventree_db.sqlite3'), }, - 'postgresql': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': 'inventree', - 'USER': 'inventreeuser', - 'PASSWORD': 'inventree', - 'HOST': 'localhost', - 'PORT': '', - } + # TODO - Uncomment this when postgresql support is re-integrated + # 'postgresql': { + # 'ENGINE': 'django.db.backends.postgresql', + # 'NAME': 'inventree', + # 'USER': 'inventreeuser', + # 'PASSWORD': 'inventree', + # 'HOST': 'localhost', + # 'PORT': '', + # } } CACHES = { diff --git a/requirements.txt b/requirements.txt index 8d8acfb510..729fa0adde 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ Django==2.2.2 # Django package -psycopg2>=2.8.1 # PostgreSQL package +# psycopg2>=2.8.1 # PostgreSQL package pillow>=5.0.0 # Image manipulation djangorestframework>=3.6.2 # DRF framework django-cors-headers>=2.5.3 # CORS headers extension for DRF From 802255c62d9ea2e3fe9883ef96098a2e731a6d5e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 2 Jul 2019 19:02:19 +1000 Subject: [PATCH 35/70] Render GET as a formview rather than ajaxview --- .../part/bom_upload/upload_file.html | 8 ++---- InvenTree/part/views.py | 26 ++++++++++++------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/InvenTree/part/templates/part/bom_upload/upload_file.html b/InvenTree/part/templates/part/bom_upload/upload_file.html index a0c218f0bf..30c7a96f94 100644 --- a/InvenTree/part/templates/part/bom_upload/upload_file.html +++ b/InvenTree/part/templates/part/bom_upload/upload_file.html @@ -8,11 +8,6 @@

    Upload Bill of Materials


    -

    - Select a BOM file to upload for:
    - {{ part.name }} - {{ part.description }} -

    -

    The BOM file must contain the required named columns as provided in the BOM Upload Template

    @@ -21,8 +16,9 @@ - {% crispy form %] + {% crispy form %} +
    {% endblock %} \ No newline at end of file diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 259549cc8d..11509871b3 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -8,8 +8,8 @@ from __future__ import unicode_literals from django.core.exceptions import ValidationError from django.shortcuts import get_object_or_404 from django.utils.translation import gettext_lazy as _ -from django.urls import reverse_lazy -from django.views.generic import DetailView, ListView +from django.urls import reverse, reverse_lazy +from django.views.generic import DetailView, ListView, FormView from django.views.generic.edit import FormMixin from django.forms.models import model_to_dict from django.forms import HiddenInput, CheckboxInput @@ -620,7 +620,7 @@ class BomValidate(AjaxUpdateView): return self.renderJsonResponse(request, form, data, context=self.get_context()) -class BomUpload(AjaxView, FormMixin): +class BomUpload(FormView): """ View for uploading a BOM file, and handling BOM data importing. The BOM upload process is as follows: @@ -647,8 +647,11 @@ class BomUpload(AjaxView, FormMixin): During these steps, data are passed between the server/client as JSON objects. """ - ajax_form_title = 'Upload Bill of Materials' - ajax_template_name = 'part/bom_upload/select_file.html' + template_name = 'part/bom_upload/select_file.html' + + def get_success_url(self): + part = self.get_object() + return reverse('upload-bom', kwargs={'pk': part.id}) def get_form_class(self): @@ -660,10 +663,11 @@ class BomUpload(AjaxView, FormMixin): # Default form is the starting point return part_forms.BomUploadSelectFile - def get_context_data(self): - ctx = { - 'part': self.part - } + def get_context_data(self, *args, **kwargs): + + ctx = super().get_context_data(*args, **kwargs) + + ctx['part'] = self.part return ctx @@ -679,7 +683,9 @@ class BomUpload(AjaxView, FormMixin): self.form = self.get_form() - return self.renderJsonResponse(request, self.form) + form_class = self.get_form_class() + form = self.get_form(form_class) + return self.render_to_response(self.get_context_data(form=form)) def handleBomFileUpload(self): """ Process a BOM file upload form. From 4008a9fb4506067757a847f4b87b49b4c79f0800 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 2 Jul 2019 19:07:59 +1000 Subject: [PATCH 36/70] Upload the selected BOM file --- .../part/bom_upload/select_file.html | 25 ------------------- .../part/bom_upload/upload_file.html | 4 ++- InvenTree/part/urls.py | 2 +- InvenTree/part/views.py | 24 ++++++------------ 4 files changed, 11 insertions(+), 44 deletions(-) delete mode 100644 InvenTree/part/templates/part/bom_upload/select_file.html diff --git a/InvenTree/part/templates/part/bom_upload/select_file.html b/InvenTree/part/templates/part/bom_upload/select_file.html deleted file mode 100644 index 68380dbfd5..0000000000 --- a/InvenTree/part/templates/part/bom_upload/select_file.html +++ /dev/null @@ -1,25 +0,0 @@ -{% extends "modal_form.html" %} - -{% block pre_form_content %} - -

    Step 1 of 3 - Select BOM File

    - -{{ block.super }} - -

    Select a BOM file to upload for:
    - {{ part.name }} - {{ part.description }} -

    - -

    The BOM file must contain the required named columns as provided in the BOM Upload Template

    - -Notes: -
      -
    • Supported file formats: .csv, .tsv, .xls, .xlsx
    • -
    • Maximum of 1000 lines per BOM
    • -
    - -{% endblock %} - -{% block form_data %} - -{% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/bom_upload/upload_file.html b/InvenTree/part/templates/part/bom_upload/upload_file.html index 30c7a96f94..3f3bc873d2 100644 --- a/InvenTree/part/templates/part/bom_upload/upload_file.html +++ b/InvenTree/part/templates/part/bom_upload/upload_file.html @@ -8,7 +8,9 @@

    Upload Bill of Materials


    -

    The BOM file must contain the required named columns as provided in the BOM Upload Template

    +
    +

    The BOM file must contain the required named columns as provided in the BOM Upload Template

    +
    {% csrf_token %} diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index 22cff6c83d..e39375f032 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -27,7 +27,7 @@ part_detail_urls = [ url(r'^make-variant/', views.MakePartVariant.as_view(), name='make-part-variant'), url(r'^pricing/', views.PartPricing.as_view(), name='part-pricing'), - url(r'^bom-upload/?', views.BomUpload.as_view(template_name='part/bom_upload/upload_file.html'), name='upload-bom'), + url(r'^bom-upload/?', views.BomUpload.as_view(), name='upload-bom'), url(r'^variants/?', views.PartDetail.as_view(template_name='part/variants.html'), name='part-variants'), url(r'^stock/?', views.PartDetail.as_view(template_name='part/stock.html'), name='part-stock'), diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 11509871b3..e33a5b3c14 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -647,8 +647,8 @@ class BomUpload(FormView): During these steps, data are passed between the server/client as JSON objects. """ - template_name = 'part/bom_upload/select_file.html' - + template_name='part/bom_upload/upload_file.html' + def get_success_url(self): part = self.get_object() return reverse('upload-bom', kwargs={'pk': part.id}) @@ -715,16 +715,9 @@ class BomUpload(FormView): for k, v in errors.items(): self.form.errors[k] = v - data = { - 'form_valid': False - } - - ctx = {} - - if bom_file_valid: + if 0 and bom_file_valid: # BOM file is valid? Proceed to the next step! form = part_forms.BomUploadSelectFields - self.ajax_template_name = 'part/bom_upload/select_fields.html' # Provide context to the next form ctx = { @@ -734,8 +727,9 @@ class BomUpload(FormView): } else: form = self.form + form.errors['bom_file'] = [_('no errors')] - return self.renderJsonResponse(self.request, form, data=data, context=ctx) + return self.render_to_response(self.get_context_data(form=form)) def handleFieldSelection(self): """ Handle the output of the field selection form. @@ -855,7 +849,7 @@ class BomUpload(FormView): self.request = request self.part = get_object_or_404(Part, pk=self.kwargs['pk']) - self.form = self.get_form() + self.form = self.get_form(self.get_form_class()) # Did the user POST a file named bom_file? @@ -866,11 +860,7 @@ class BomUpload(FormView): elif form_step == 'select_fields': return self.handleFieldSelection() - data = { - 'form_valid': False, - } - - return self.renderJsonResponse(request, self.form, data=data) + return self.render_to_response(self.get_context_data(form=self.form)) class BomUploadTemplate(AjaxView): From fc5682f5657c788904e0404e28e58e5daae8f5cc Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 2 Jul 2019 19:20:45 +1000 Subject: [PATCH 37/70] Form is now fully transferred to a formview --- InvenTree/part/bom.py | 2 +- InvenTree/part/templates/part/bom.html | 5 ---- .../part/bom_upload/select_fields.html | 19 +++++++----- .../part/bom_upload/upload_file.html | 5 +++- InvenTree/part/views.py | 29 ++++++++++++++----- InvenTree/templates/base.html | 1 + 6 files changed, 40 insertions(+), 21 deletions(-) diff --git a/InvenTree/part/bom.py b/InvenTree/part/bom.py index 70cea0601b..79d1bc9e5d 100644 --- a/InvenTree/part/bom.py +++ b/InvenTree/part/bom.py @@ -122,7 +122,7 @@ class BomUploadManager: return None - def get_headers(self): + def columns(self): """ Return a list of headers for the thingy """ headers = [] diff --git a/InvenTree/part/templates/part/bom.html b/InvenTree/part/templates/part/bom.html index ea99c16323..3d61e24e2a 100644 --- a/InvenTree/part/templates/part/bom.html +++ b/InvenTree/part/templates/part/bom.html @@ -62,11 +62,6 @@ {% endblock %} -{% block js_load %} -{{ block.super }} - -{% endblock %} - {% block js_ready %} {{ block.super }} diff --git a/InvenTree/part/templates/part/bom_upload/select_fields.html b/InvenTree/part/templates/part/bom_upload/select_fields.html index ad90530ad8..c423d7b0c1 100644 --- a/InvenTree/part/templates/part/bom_upload/select_fields.html +++ b/InvenTree/part/templates/part/bom_upload/select_fields.html @@ -1,16 +1,20 @@ -{% extends "modal_form.html" %} +{% extends "part/part_base.html" %} +{% load static %} {% load inventree_extras %} -{% block form %} +{% block details %} +{% include "part/tabs.html" with tab='bom' %} +

    Upload Bill of Materials

    -

    Step 2 of 3 - Select BOM Fields

    +

    Step 2 - Select Fields

    +
    {% if missing and missing|length > 0 %}
    Row
    @@ -45,11 +49,11 @@
    @@ -73,6 +77,7 @@
    + BOM Rows: {{ bom.row_count }} diff --git a/InvenTree/part/templates/part/bom_upload/upload_file.html b/InvenTree/part/templates/part/bom_upload/upload_file.html index 3f3bc873d2..482d893db2 100644 --- a/InvenTree/part/templates/part/bom_upload/upload_file.html +++ b/InvenTree/part/templates/part/bom_upload/upload_file.html @@ -1,5 +1,6 @@ {% extends "part/part_base.html" %} {% load static %} +{% load inventree_extras %} {% block details %} @@ -8,6 +9,8 @@

    Upload Bill of Materials


    +

    Step 1 - Select BOM File

    +

    The BOM file must contain the required named columns as provided in the BOM Upload Template

    @@ -20,7 +23,7 @@ {% crispy form %} - + {% endblock %} \ No newline at end of file diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index e33a5b3c14..2865fb5792 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -648,6 +648,13 @@ class BomUpload(FormView): """ template_name='part/bom_upload/upload_file.html' + + # Context data passed to the forms (initially empty, extracted from uploaded file) + bom_headers = [] + bom_columns = [] + bom_rows = [] + missing_columns = [] + def get_success_url(self): part = self.get_object() @@ -668,6 +675,10 @@ class BomUpload(FormView): ctx = super().get_context_data(*args, **kwargs) ctx['part'] = self.part + ctx['bom_headers'] = self.bom_headers + ctx['bom_columns'] = self.bom_columns + ctx['bom_rows'] = self.bom_rows + ctx['missing_columns'] = self.missing_columns return ctx @@ -715,22 +726,26 @@ class BomUpload(FormView): for k, v in errors.items(): self.form.errors[k] = v - if 0 and bom_file_valid: + if bom_file_valid: # BOM file is valid? Proceed to the next step! form = part_forms.BomUploadSelectFields + self.template_name = 'part/bom_upload/select_fields.html' - # Provide context to the next form - ctx = { - 'req_cols': BomUploadManager.HEADERS, - 'bom_cols': manager.get_headers(), - 'bom_rows': manager.rows(), - } + self.extractDataFromFile(manager) else: form = self.form form.errors['bom_file'] = [_('no errors')] return self.render_to_response(self.get_context_data(form=form)) + def extractDataFromFile(self, bom): + """ Read data from the BOM file """ + + self.bom_headers = bom.HEADERS + self.bom_columns = bom.columns() + self.bom_rows = bom.rows() + + def handleFieldSelection(self): """ Handle the output of the field selection form. Here the user is presented with the raw data and must select the diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index fee348c7e5..51f396df8b 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -99,6 +99,7 @@ InvenTree + From c959e8f62c7e5266c9d55c8b6eead1e5ddbfe6ce Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 2 Jul 2019 19:45:26 +1000 Subject: [PATCH 38/70] Add ability to remove individual rows from BOM uploader --- .../part/bom_upload/select_fields.html | 10 ++++-- InvenTree/static/script/inventree/bom.js | 31 +++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/InvenTree/part/templates/part/bom_upload/select_fields.html b/InvenTree/part/templates/part/bom_upload/select_fields.html index c423d7b0c1..366cce80a3 100644 --- a/InvenTree/part/templates/part/bom_upload/select_fields.html +++ b/InvenTree/part/templates/part/bom_upload/select_fields.html @@ -32,13 +32,14 @@ + {% for col in bom_columns %} {% for row in bom_rows %} - + + {% for item in row.data %}
    Row
    {{ col.name }} -
    @@ -65,7 +66,12 @@
    {% add forloop.counter 1 %} + + {{ forloop.counter }} diff --git a/InvenTree/static/script/inventree/bom.js b/InvenTree/static/script/inventree/bom.js index 581fb8d4a3..8b6e802f72 100644 --- a/InvenTree/static/script/inventree/bom.js +++ b/InvenTree/static/script/inventree/bom.js @@ -12,6 +12,37 @@ function reloadBomTable(table, options) { } +function removeRowFromBomWizard(e) { + /* Remove a row from BOM upload wizard + */ + + e = e || window.event; + + var src = e.target || e.srcElement; + + var table = $(src).closest('table'); + + // Which column was clicked? + var row = $(src).closest('tr'); + + row.remove(); + + var rowNum = 1; + var colNum = 0; + + table.find('tr').each(function() { + + colNum++; + + if (colNum >= 3) { + var cell = $(this).find('td:eq(1)'); + cell.text(rowNum++); + console.log("Row: " + rowNum); + } + }); +} + + function removeColFromBomWizard(e) { /* Remove a column from BOM upload wizard */ From a25522746ee59c98aa501a13cfcbbd82fbb45093 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 2 Jul 2019 19:46:39 +1000 Subject: [PATCH 39/70] Reposition buttons --- InvenTree/part/templates/part/bom_upload/select_fields.html | 2 +- InvenTree/part/templates/part/bom_upload/upload_file.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/part/templates/part/bom_upload/select_fields.html b/InvenTree/part/templates/part/bom_upload/select_fields.html index 366cce80a3..afeca4248c 100644 --- a/InvenTree/part/templates/part/bom_upload/select_fields.html +++ b/InvenTree/part/templates/part/bom_upload/select_fields.html @@ -22,6 +22,7 @@ {% endif %}
    + {% csrf_token %} {% load crispy_forms_tags %} @@ -83,7 +84,6 @@
    - BOM Rows: {{ bom.row_count }} diff --git a/InvenTree/part/templates/part/bom_upload/upload_file.html b/InvenTree/part/templates/part/bom_upload/upload_file.html index 482d893db2..37c5801b5c 100644 --- a/InvenTree/part/templates/part/bom_upload/upload_file.html +++ b/InvenTree/part/templates/part/bom_upload/upload_file.html @@ -16,6 +16,7 @@
    + {% csrf_token %} {% load crispy_forms_tags %} @@ -23,7 +24,6 @@ {% crispy form %} -
    {% endblock %} \ No newline at end of file From c419207420b11a960ed4ff0bc87ba102526a2092 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 2 Jul 2019 19:48:30 +1000 Subject: [PATCH 40/70] Insert a blank as needed --- InvenTree/part/templates/part/bom_upload/select_fields.html | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/part/templates/part/bom_upload/select_fields.html b/InvenTree/part/templates/part/bom_upload/select_fields.html index afeca4248c..a990967cb6 100644 --- a/InvenTree/part/templates/part/bom_upload/select_fields.html +++ b/InvenTree/part/templates/part/bom_upload/select_fields.html @@ -50,6 +50,7 @@ + {% for col in bom_columns %} From 29a27ce5981d18a9443f7abb88bdcf4576324a07 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 3 Jul 2019 20:08:49 +1000 Subject: [PATCH 41/70] Improve rendering for field selection form --- InvenTree/part/forms.py | 2 -- InvenTree/part/views.py | 34 ++++++++++------------------------ 2 files changed, 10 insertions(+), 26 deletions(-) diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index 09e9c75f3b..38280bbc1c 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -55,13 +55,11 @@ class BomUploadSelectFields(HelperForm): """ Form for selecting BOM fields """ starting_row = forms.IntegerField(required=True, initial=2, help_text='Index of starting row', validators=[MinValueValidator(1)]) - row_count = forms.IntegerField(required=True, help_text='Number of rows to process', validators=[MinValueValidator(0)]) class Meta: model = Part fields = [ 'starting_row', - 'row_count', ] diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 2865fb5792..eb575986cc 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -675,7 +675,7 @@ class BomUpload(FormView): ctx = super().get_context_data(*args, **kwargs) ctx['part'] = self.part - ctx['bom_headers'] = self.bom_headers + ctx['bom_headers'] = BomUploadManager.HEADERS ctx['bom_columns'] = self.bom_columns ctx['bom_rows'] = self.bom_rows ctx['missing_columns'] = self.missing_columns @@ -741,7 +741,6 @@ class BomUpload(FormView): def extractDataFromFile(self, bom): """ Read data from the BOM file """ - self.bom_headers = bom.HEADERS self.bom_columns = bom.columns() self.bom_rows = bom.rows() @@ -752,12 +751,6 @@ class BomUpload(FormView): column names and which rows to process. """ - data = { - 'form_valid': False, - } - - self.ajax_template_name = 'part/bom_upload/select_fields.html' - # Map the columns column_names = {} column_selections = {} @@ -801,7 +794,7 @@ class BomUpload(FormView): col_ids = sorted(column_names.keys()) - headers = [] + self.bom_columns = [] for col in col_ids: if col not in column_selections: @@ -820,17 +813,17 @@ class BomUpload(FormView): if n > 1: header['duplicate'] = True - headers.append(header) + self.bom_columns.append(header) # Are there any missing columns? - missing = [] + self.missing_columns = [] for col in BomUploadManager.REQUIRED_HEADERS: if col not in column_selections.values(): - missing.append(col) + self.missing_columns.append(col) # Re-construct the data table - rows = [] + self.bom_rows = [] for row_idx in sorted(row_data.keys()): row = row_data[row_idx] @@ -843,19 +836,12 @@ class BomUpload(FormView): value = row[col_idx] items.append(value) - rows.append({'index': row_idx, 'data': items}) + self.bom_rows.append({'index': row_idx, 'data': items}) - ctx = { - # The headers that we know about - 'req_cols': BomUploadManager.HEADERS, - 'bom_cols': headers, - 'missing': missing, - 'bom_rows': rows, - } + form = part_forms.BomUploadSelectFields + self.template_name = 'part/bom_upload/select_fields.html' - print(ctx) - - return self.renderJsonResponse(self.request, form=self.get_form(), data=data, context=ctx) + return self.render_to_response(self.get_context_data(form=form)) def post(self, request, *args, **kwargs): """ Perform the various 'POST' requests required. From 064431e94f3b5cb085e4e83737333460310c13aa Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 3 Jul 2019 20:14:13 +1000 Subject: [PATCH 42/70] Fix template to display list of missing BOM columns --- InvenTree/part/templates/part/bom_upload/select_fields.html | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/InvenTree/part/templates/part/bom_upload/select_fields.html b/InvenTree/part/templates/part/bom_upload/select_fields.html index a990967cb6..45f2b8c3a3 100644 --- a/InvenTree/part/templates/part/bom_upload/select_fields.html +++ b/InvenTree/part/templates/part/bom_upload/select_fields.html @@ -9,7 +9,7 @@

    Step 2 - Select Fields


    -{% if missing and missing|length > 0 %} +{% if missing_columns and missing_columns|length > 0 %}