diff --git a/InvenTree/common/files.py b/InvenTree/common/files.py index 1964e662e2..ab73ec4f1e 100644 --- a/InvenTree/common/files.py +++ b/InvenTree/common/files.py @@ -18,25 +18,17 @@ class FileManager: name = '' # Fields which are absolutely necessary for valid upload - REQUIRED_HEADERS = [ - 'Quantity' - ] + REQUIRED_HEADERS = [] # Fields which are used for part matching (only one of them is needed) - PART_MATCH_HEADERS = [ - 'Part_Name', - 'Part_IPN', - 'Part_ID', - ] + PART_MATCH_HEADERS = [] # Fields which would be helpful but are not required - OPTIONAL_HEADERS = [ - ] + OPTIONAL_HEADERS = [] - EDITABLE_HEADERS = [ - ] + EDITABLE_HEADERS = [] - HEADERS = REQUIRED_HEADERS + PART_MATCH_HEADERS + OPTIONAL_HEADERS + HEADERS = [] def __init__(self, file, name=None): """ Initialize the FileManager class with a user-uploaded file object """ @@ -48,6 +40,9 @@ class FileManager: # Process initial file self.process(file) + # Update headers + self.update_headers() + def process(self, file): """ Process file """ @@ -69,6 +64,37 @@ class FileManager: raise ValidationError(_(f'Error reading {self.name} file (invalid format)')) except tablib.core.InvalidDimensions: raise ValidationError(_(f'Error reading {self.name} file (incorrect dimension)')) + + def update_headers(self): + """ Update headers """ + + self.HEADERS = self.REQUIRED_HEADERS + self.PART_MATCH_HEADERS + self.OPTIONAL_HEADERS + + def setup(self): + """ Setup headers depending on the file name """ + + if not self.name: + return False + + if self.name == 'order': + self.REQUIRED_HEADERS = [ + 'Quantity', + ] + + self.PART_MATCH_HEADERS = [ + 'Manufacturer_MPN', + 'Supplier_SKU', + ] + + self.OPTIONAL_HEADERS = [ + 'Unit_Price', + 'Extended_Price', + ] + + # Update headers + self.update_headers() + + return True def guess_header(self, header, threshold=80): """ Try to match a header (from the file) to a list of known headers diff --git a/InvenTree/common/views.py b/InvenTree/common/views.py index 8220b00554..af3f7fea94 100644 --- a/InvenTree/common/views.py +++ b/InvenTree/common/views.py @@ -117,22 +117,45 @@ class MultiStepFormView(SessionWizardView): """ form_list = [] + form_steps_template = [] form_steps_description = [] + file_manager = None media_folder = '' file_storage = FileSystemStorage(settings.MEDIA_ROOT) def __init__(self, *args, **kwargs): + """ Override init method to set media folder """ super().__init__(*args, **kwargs) + self.process_media_folder() + + def process_media_folder(self): + """ Process media folder """ + if self.media_folder: media_folder_abs = os.path.join(settings.MEDIA_ROOT, self.media_folder) if not os.path.exists(media_folder_abs): os.mkdir(media_folder_abs) self.file_storage = FileSystemStorage(location=media_folder_abs) + def get_template_names(self): + """ Select template """ + + try: + # Get template + template = self.form_steps_template[int(self.steps.current)] + except IndexError: + return self.template_name + + return template + def get_context_data(self, **kwargs): + """ Update context data """ + + # Retrieve current context context = super().get_context_data(**kwargs) - # Get description + + # Get form description try: description = self.form_steps_description[int(self.steps.current)] except IndexError: diff --git a/InvenTree/order/forms.py b/InvenTree/order/forms.py index 5f6253217c..b1e5745937 100644 --- a/InvenTree/order/forms.py +++ b/InvenTree/order/forms.py @@ -309,9 +309,9 @@ class UploadFile(forms.Form): class MatchField(forms.Form): """ Step 2 """ - last_name = forms.CharField(max_length=100) + pass class MatchPart(forms.Form): """ Step 3 """ - age = forms.IntegerField() + pass diff --git a/InvenTree/order/templates/order/order_wizard/match_fields.html b/InvenTree/order/templates/order/order_wizard/match_fields.html new file mode 100644 index 0000000000..818f986850 --- /dev/null +++ b/InvenTree/order/templates/order/order_wizard/match_fields.html @@ -0,0 +1,89 @@ +{% extends "order/order_wizard/po_upload.html" %} +{% load inventree_extras %} +{% load i18n %} +{% load static %} + +{% block form %} + +{% if missing_columns and missing_columns|length > 0 %} + +{% endif %} + +
+ {% csrf_token %} + {% load crispy_forms_tags %} + {{ wizard.management_form }} + + {% if wizard.steps.prev %} + + {% endif %} + + + + + + + + {% for col in columns %} + + {% endfor %} + + + + + + + {% for col in columns %} + + {% endfor %} + + {% for row in rows %} + {% with forloop.counter as row_index %} + + + + {% for item in row.data %} + + {% endfor %} + + {% endwith %} + {% endfor %} + +
{% trans "File Fields" %} +
+ + {{ col.name }} + +
+
{% trans "Match Fields" %} + + {% if col.duplicate %} +

{% trans "Duplicate column selection" %}

+ {% endif %} +
+ + {{ row_index }} + + {{ item }} +
+ +
+ +{% endblock %} \ No newline at end of file diff --git a/InvenTree/order/templates/order/order_wizard/match_parts.html b/InvenTree/order/templates/order/order_wizard/match_parts.html new file mode 100644 index 0000000000..a1c5103e9e --- /dev/null +++ b/InvenTree/order/templates/order/order_wizard/match_parts.html @@ -0,0 +1,112 @@ +{% extends "order/order_wizard/po_upload.html" %} +{% load inventree_extras %} +{% load i18n %} +{% load static %} + +{% block form %} + +{% if form_errors %} + +{% endif %} + +
+ {% csrf_token %} + {% load crispy_forms_tags %} + {{ wizard.management_form }} + + {% if wizard.steps.prev %} + + {% endif %} + + {% csrf_token %} + + + + + + + + + {% for col in bom_columns %} + + {% endfor %} + + + + {% for row in bom_rows %} + + + + + + {% for item in row.data %} + + {% endfor %} + + {% endfor %} + +
{% trans "Row" %}{% trans "Select Part" %} + + + {% if col.guess %} + {{ col.guess }} + {% else %} + {{ col.name }} + {% endif %} +
+ + {% add row.index 1 %} + + + {% if row.errors.part %} +

{{ row.errors.part }}

+ {% endif %} +
+ {% if item.column.guess == 'Part' %} + {{ item.cell }} + {% if row.errors.part %} +

{{ row.errors.part }}

+ {% endif %} + {% elif item.column.guess == 'Quantity' %} + + {% if row.errors.quantity %} +

{{ row.errors.quantity }}

+ {% endif %} + {% elif item.column.guess == 'Reference' %} + + {% elif item.column.guess == 'Note' %} + + {% elif item.column.guess == 'Overage' %} + + {% else %} + {{ item.cell }} + {% endif %} + +
+ +
+ +{% endblock %} + +{% block js_ready %} +{{ block.super }} + +$('.bomselect').select2({ + dropdownAutoWidth: true, + matcher: partialMatcher, +}); + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/order/templates/order/po_upload.html b/InvenTree/order/templates/order/order_wizard/po_upload.html similarity index 80% rename from InvenTree/order/templates/order/po_upload.html rename to InvenTree/order/templates/order/order_wizard/po_upload.html index 9650ee763d..2c7c49b662 100644 --- a/InvenTree/order/templates/order/po_upload.html +++ b/InvenTree/order/templates/order/order_wizard/po_upload.html @@ -1,5 +1,4 @@ {% extends "order/order_base.html" %} - {% load inventree_extras %} {% load i18n %} {% load static %} @@ -17,7 +16,11 @@

{% trans "Step" %} {{ wizard.steps.step1 }} {% trans "of" %} {{ wizard.steps.count }} {% if description %}- {{ description }}{% endif %}

-
{% csrf_token %} + +{% block form %} + + +{% csrf_token %} {% load crispy_forms_tags %} {{ wizard.management_form }} @@ -29,15 +32,16 @@ {% else %} {% crispy wizard.form %} {% endif %} -
-{% if wizard.steps.prev %} +{% if wizard.steps.prev %} {% endif %} - +
+ -{% endblock %} +{% endblock form %} +{% endblock details %} {% block js_ready %} {{ block.super }} diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index ec9a4a4742..f9eb94eaa2 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -567,21 +567,30 @@ class SalesOrderShip(AjaxUpdateView): class PurchaseOrderUpload(MultiStepFormView): - ''' PurchaseOrder: Upload file and match to parts, using multi-Step form ''' + ''' PurchaseOrder: Upload file, match to fields and parts (using multi-Step form) ''' form_list = [ order_forms.UploadFile, order_forms.MatchField, order_forms.MatchPart, ] + form_steps_template = [ + 'order/order_wizard/po_upload.html', + 'order/order_wizard/match_fields.html', + 'order/order_wizard/match_parts.html', + ] form_steps_description = [ _("Upload File"), - _("Select Fields"), - _("Select Parts"), + _("Match Fields"), + _("Match Parts"), ] - template_name = "order/po_upload.html" media_folder = 'order_uploads/' + # Used for data table + headers = None + rows = None + columns = None + def get_context_data(self, form, **kwargs): context = super().get_context_data(form=form, **kwargs) @@ -589,25 +598,52 @@ class PurchaseOrderUpload(MultiStepFormView): context.update({'order': order}) + if self.headers: + context.update({'headers': self.headers}) + # print(f'{self.headers}') + if self.columns: + context.update({'columns': self.columns}) + # print(f'{self.columns}') + if self.rows: + context.update({'rows': self.rows}) + # print(f'{self.rows}') + return context - def get_form_step_data(self, form): - # print(f'{self.steps.current=}\n{form.data=}') - return form.data + def process_step(self, form): + print(f'{self.steps.current=} | {form.data}') + return self.get_form_step_data(form) + + # def get_all_cleaned_data(self): + # cleaned_data = super().get_all_cleaned_data() + # print(f'{self.steps.current=} | {cleaned_data}') + # return cleaned_data + + # def get_form_step_data(self, form): + # print(f'{self.steps.current=} | {form.data}') + # return form.data def get_form_step_files(self, form): # Check if user completed file upload if self.steps.current == '0': - # Extract columns and rows from FileManager - self.extractDataFromFile(form.file_manager) + # Retrieve FileManager instance from form + self.file_manager = form.file_manager + # Setup FileManager for order upload + setup_valid = self.file_manager.setup() + if setup_valid: + # Set headers + self.headers = self.file_manager.HEADERS + # Set columns and rows + self.columns = self.file_manager.columns() + self.rows = self.file_manager.rows() return form.files - def extractDataFromFile(self, file_manager): - """ Read data from the file """ - - self.columns = file_manager.columns() - self.rows = file_manager.rows() + def post(self, request, *args, **kwargs): + """ Perform the various 'POST' requests required. + """ + print('Posting!') + return super().post(*args, **kwargs) def done(self, form_list, **kwargs): return HttpResponseRedirect(reverse('po-detail', kwargs={'pk': self.kwargs['pk']}))