From 307a097ab4685d45bbf7930a728740979b6db986 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 May 2021 00:56:14 +0200 Subject: [PATCH 01/68] adding in url --- InvenTree/part/urls.py | 3 +++ InvenTree/part/views.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index c734b7f610..e2b172780a 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -120,6 +120,9 @@ part_urls = [ # Create a new part url(r'^new/?', views.PartCreate.as_view(), name='part-create'), + # Upload a part + url(r'^import/', views.PartImport.as_view(), name='part-upload'), + # Create a new BOM item url(r'^bom/new/?', views.BomItemCreate.as_view(), name='bom-item-create'), diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 2fabdb9fc8..d9b0b51693 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -39,6 +39,7 @@ from .models import PartSellPriceBreak from common.models import InvenTreeSetting from company.models import SupplierPart +from common.views import FileManagementFormView import common.settings as inventree_settings @@ -718,6 +719,9 @@ class PartCreate(AjaxCreateView): return initials +class PartImport(FileManagementFormView): + ''' Part: Upload file, match to fields and import parts(using multi-Step form) ''' + class PartNotes(UpdateView): """ View for editing the 'notes' field of a Part object. Presents a live markdown editor. From 8effdffe6f8f43491d5b97e37d6ca3137a997f40 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 May 2021 00:57:18 +0200 Subject: [PATCH 02/68] templates and FileManagement config --- InvenTree/common/files.py | 21 ++++ .../part/import_wizard/match_fields.html | 99 +++++++++++++++++++ .../part/import_wizard/match_references.html | 90 +++++++++++++++++ .../part/import_wizard/part_upload.html | 61 ++++++++++++ InvenTree/part/views.py | 25 +++++ 5 files changed, 296 insertions(+) create mode 100644 InvenTree/part/templates/part/import_wizard/match_fields.html create mode 100644 InvenTree/part/templates/part/import_wizard/match_references.html create mode 100644 InvenTree/part/templates/part/import_wizard/part_upload.html diff --git a/InvenTree/common/files.py b/InvenTree/common/files.py index 377120f44d..759ff04ee7 100644 --- a/InvenTree/common/files.py +++ b/InvenTree/common/files.py @@ -109,6 +109,27 @@ class FileManager: # Update headers self.update_headers() + if self.name == 'part': + self.REQUIRED_HEADERS = [ + 'Name', + 'Description', + ] + + self.OPTIONAL_HEADERS = [ + 'Keywords', + 'IPN', + 'Revision', + 'Link', + 'default_expiry', + 'minimum_stock', + 'Units', + 'Notes', + ] + + # Update headers + self.update_headers() + + 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/part/templates/part/import_wizard/match_fields.html b/InvenTree/part/templates/part/import_wizard/match_fields.html new file mode 100644 index 0000000000..54008d6bae --- /dev/null +++ b/InvenTree/part/templates/part/import_wizard/match_fields.html @@ -0,0 +1,99 @@ +{% extends "part/import_wizard/part_upload.html" %} +{% load inventree_extras %} +{% load i18n %} +{% load static %} + +{% block form_alert %} +{% if missing_columns and missing_columns|length > 0 %} + +{% endif %} +{% if duplicates and duplicates|length > 0 %} + +{% endif %} +{% endblock form_alert %} + +{% block form_buttons_top %} + {% if wizard.steps.prev %} + + {% endif %} + +{% endblock form_buttons_top %} + +{% block form_content %} + + + {% trans "File Fields" %} + + {% for col in form %} + +
+ + {{ col.name }} + +
+ + {% endfor %} + + + + + {% trans "Match Fields" %} + + {% for col in form %} + + {{ col }} + {% for duplicate in duplicates %} + {% if duplicate == col.name %} + + {% endif %} + {% endfor %} + + {% endfor %} + + {% for row in rows %} + {% with forloop.counter as row_index %} + + + + + {{ row_index }} + {% for item in row.data %} + + + {{ item }} + + {% endfor %} + + {% endwith %} + {% endfor %} + +{% endblock form_content %} + +{% block form_buttons_bottom %} +{% endblock form_buttons_bottom %} + +{% block js_ready %} +{{ block.super }} + +$('.fieldselect').select2({ + width: '100%', + matcher: partialMatcher, +}); + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/import_wizard/match_references.html b/InvenTree/part/templates/part/import_wizard/match_references.html new file mode 100644 index 0000000000..efc69b98d5 --- /dev/null +++ b/InvenTree/part/templates/part/import_wizard/match_references.html @@ -0,0 +1,90 @@ +{% extends "part/import_wizard/part_upload.html" %} +{% load inventree_extras %} +{% load i18n %} +{% load static %} + +{% block form_alert %} +{% if form.errors %} +{% endif %} +{% if form_errors %} + +{% endif %} +{% endblock form_alert %} + +{% block form_buttons_top %} + {% if wizard.steps.prev %} + + {% endif %} + +{% endblock form_buttons_top %} + +{% block form_content %} + + + + {% trans "Row" %} + {% for col in columns %} + + + + + {% if col.guess %} + {{ col.guess }} + {% else %} + {{ col.name }} + {% endif %} + + {% endfor %} + + + + {% comment %} Dummy row for javascript del_row method {% endcomment %} + {% for row in rows %} + + + + + + {% add row.index 1 %} + + {% for item in row.data %} + + {% if item.column.guess %} + {% with row_name=item.column.guess|lower %} + {% for field in form.visible_fields %} + {% if field.name == row|keyvalue:row_name %} + {{ field }} + {% endif %} + {% endfor %} + {% endwith %} + {% else %} + {{ item.cell }} + {% endif %} + + + {% endfor %} + + {% endfor %} + +{% endblock form_content %} + +{% block form_buttons_bottom %} +{% endblock form_buttons_bottom %} + +{% block js_ready %} +{{ block.super }} + +$('.bomselect').select2({ + dropdownAutoWidth: true, + matcher: partialMatcher, +}); + +$('.currencyselect').select2({ + dropdownAutoWidth: true, +}); + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/import_wizard/part_upload.html b/InvenTree/part/templates/part/import_wizard/part_upload.html new file mode 100644 index 0000000000..87809603bb --- /dev/null +++ b/InvenTree/part/templates/part/import_wizard/part_upload.html @@ -0,0 +1,61 @@ +{% extends "part/category.html" %} +{% load inventree_extras %} +{% load i18n %} +{% load static %} + +{% block menubar %} +{% include 'part/category_navbar.html' with tab='import' %} +{% endblock %} + +{% block category_content %} +
+
+

+ {% trans "Import Parts from File" %} + {{ wizard.form.media }} +

+
+
+ {% if roles.part.change %} + +

{% blocktrans with step=wizard.steps.step1 count=wizard.steps.count %}Step {{step}} of {{count}}{% endblocktrans %} + {% if description %}- {{ description }}{% endif %}

+ + {% block form_alert %} + {% endblock form_alert %} + +
+ {% csrf_token %} + {% load crispy_forms_tags %} + + {% block form_buttons_top %} + {% endblock form_buttons_top %} + + + {{ wizard.management_form }} + {% block form_content %} + {% crispy wizard.form %} + {% endblock form_content %} +
+ + {% block form_buttons_bottom %} + {% if wizard.steps.prev %} + + {% endif %} + +
+ {% endblock form_buttons_bottom %} + + {% else %} + + {% endif %} +
+
+{% endblock %} + +{% block js_ready %} +{{ block.super }} + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index d9b0b51693..1e19387e81 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -722,6 +722,31 @@ class PartCreate(AjaxCreateView): class PartImport(FileManagementFormView): ''' Part: Upload file, match to fields and import parts(using multi-Step form) ''' + name = 'part' + form_steps_template = [ + 'part/import_wizard/part_upload.html', + 'part/import_wizard/match_fields.html', + 'part/import_wizard/match_references.html', + ] + form_steps_description = [ + _("Upload File"), + _("Match Fields"), + _("Match References"), + ] + + form_field_map = { + 'name': 'name', + 'description': 'description', + 'keywords': 'keywords', + 'IPN': 'IPN', + 'revision': 'revision', + 'link': 'link', + 'default_expiry': 'default_expiry', + 'minimum_stock': 'minimum_stock', + 'units': 'units', + 'notes': 'notes', + } + class PartNotes(UpdateView): """ View for editing the 'notes' field of a Part object. Presents a live markdown editor. From 4ae7debb2b7f61fbe9c75bdafea630d6b7911a04 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 May 2021 00:57:38 +0200 Subject: [PATCH 03/68] navigation --- InvenTree/part/templates/part/category_navbar.html | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/InvenTree/part/templates/part/category_navbar.html b/InvenTree/part/templates/part/category_navbar.html index e723db358d..e5fb13bbda 100644 --- a/InvenTree/part/templates/part/category_navbar.html +++ b/InvenTree/part/templates/part/category_navbar.html @@ -30,6 +30,13 @@ +
  • + + + {% trans "Upload File" %} + +
  • + {% if category %}
  • From 0c5fa5777029588db382dbceef018e19ecd504ca Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 May 2021 00:58:03 +0200 Subject: [PATCH 04/68] template tag for dict-reading --- InvenTree/part/templatetags/inventree_extras.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py index 2c588a81e5..6ac34f9d5e 100644 --- a/InvenTree/part/templatetags/inventree_extras.py +++ b/InvenTree/part/templatetags/inventree_extras.py @@ -162,6 +162,11 @@ def get_color_theme_css(username): return inventree_css_static_url +@register.filter +def keyvalue(dict, key): + return dict[key] + + @register.simple_tag() def authorized_owners(group): """ Return authorized owners """ From f136f90e129b3fb44bc98d96927a9835367c174f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 May 2021 00:59:47 +0200 Subject: [PATCH 05/68] config for all form-fields --- InvenTree/common/forms.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/InvenTree/common/forms.py b/InvenTree/common/forms.py index 8a0017e38b..c869d4a4f3 100644 --- a/InvenTree/common/forms.py +++ b/InvenTree/common/forms.py @@ -151,6 +151,8 @@ class MatchItem(forms.Form): # Set field name field_name = col_guess.lower() + '-' + str(row['index']) # Set field input box + + # TODO maybe not here but in an own function? if 'quantity' in col_guess.lower(): self.fields[field_name] = forms.CharField( required=False, @@ -164,6 +166,15 @@ class MatchItem(forms.Form): }) ) + else: + # Get value + value = row.get(col_guess.lower(), '') + # Set field input box + self.fields[field_name] = forms.CharField( + required=True, + initial=value, + ) + # Create item selection box elif col_guess in file_manager.ITEM_MATCH_HEADERS: # Get item options From 888154e30bd46ef6a052b92050298a7644081d02 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 May 2021 01:01:57 +0200 Subject: [PATCH 06/68] added todo for cleaner implemention --- InvenTree/common/files.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/common/files.py b/InvenTree/common/files.py index 759ff04ee7..c4b40e1e2b 100644 --- a/InvenTree/common/files.py +++ b/InvenTree/common/files.py @@ -109,6 +109,7 @@ class FileManager: # Update headers self.update_headers() + # TODO maybe not here but in an own function? if self.name == 'part': self.REQUIRED_HEADERS = [ 'Name', From f1f75b45cbc3c9b90718bdc176040f5ccd665dc5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 May 2021 01:02:25 +0200 Subject: [PATCH 07/68] using messages for alerts --- InvenTree/InvenTree/settings.py | 7 +++++++ InvenTree/part/templates/part/category.html | 9 +++++++++ 2 files changed, 16 insertions(+) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 7ff90fc7c3..3f9c2d6cec 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -21,6 +21,7 @@ from datetime import datetime import yaml from django.utils.translation import gettext_lazy as _ +from django.contrib.messages import constants as messages def _is_true(x): @@ -593,3 +594,9 @@ IMPORT_EXPORT_USE_TRANSACTIONS = True INTERNAL_IPS = [ '127.0.0.1', ] + +MESSAGE_TAGS = { + messages.SUCCESS: 'alert alert-block alert-success', + messages.ERROR: 'alert alert-block alert-danger', + messages.INFO: 'alert alert-block alert-info', +} diff --git a/InvenTree/part/templates/part/category.html b/InvenTree/part/templates/part/category.html index b79ee0ee60..0b7fbba8f5 100644 --- a/InvenTree/part/templates/part/category.html +++ b/InvenTree/part/templates/part/category.html @@ -8,6 +8,15 @@ {% block content %} +{% if messages %} + {% for message in messages %} +
    + {{ message|safe }} +
    + {% endfor %} +{% endif %} + +
    From 437e75c598df179a4249745be34f46b3d1e83f09 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 May 2021 01:09:29 +0200 Subject: [PATCH 08/68] form functions --- InvenTree/part/views.py | 45 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 1e19387e81..7c1f1d0325 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -747,6 +747,51 @@ class PartImport(FileManagementFormView): 'notes': 'notes', } + def get_field_selection(self): + """ Fill the form fields for step 3 """ + + # collect reference indexes + idx_s = {} + for col in self.file_manager.HEADERS: + index = self.get_column_index(col) + if index >= 0: + idx_s[col] = index + + for row in self.rows: + for idx in idx_s: + data = row['data'][idx_s[idx]]['cell'] + row[idx.lower()] = data + + + def done(self, form_list, **kwargs): + """ Create items """ + items = {} + + for form_key, form_value in self.get_all_cleaned_data().items(): + # Split key from row value + try: + (field, idx) = form_key.split('-') + except ValueError: + continue + + try: + if idx not in items: + # Insert into items + items.update({ + idx: { + self.form_field_map[field]: form_value, + } + }) + else: + # Update items + items[idx][self.form_field_map[field]] = form_value + except KeyError: + pass + + + return HttpResponseRedirect(reverse('part-index')) + + class PartNotes(UpdateView): """ View for editing the 'notes' field of a Part object. Presents a live markdown editor. From 6c2e18dd7a31da3dead3f88ea3431bfe5fd9d068 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 May 2021 01:09:50 +0200 Subject: [PATCH 09/68] part creation + alerts --- InvenTree/part/views.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 7c1f1d0325..3170a48c16 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -17,6 +17,7 @@ from django.views.generic import DetailView, ListView, FormView, UpdateView from django.forms.models import model_to_dict from django.forms import HiddenInput, CheckboxInput from django.conf import settings +from django.contrib import messages from moneyed import CURRENCIES @@ -788,6 +789,36 @@ class PartImport(FileManagementFormView): except KeyError: pass + import_done = 0 + import_error = [] + + # Create Part instances + for part_data in items.values(): + new_part = Part( + name=part_data.get('name', ''), + description=part_data.get('description', ''), + keywords=part_data.get('keywords', None), + IPN=part_data.get('ipn', None), + revision=part_data.get('revision', None), + link=part_data.get('link', None), + default_expiry=part_data.get('default_expiry', 0), + minimum_stock=part_data.get('minimum_stock', 0), + units=part_data.get('units', None), + notes=part_data.get('notes', None), + ) + try: + new_part.save() + import_done += 1 + except ValidationError as _e: + import_error.append(', '.join(set(_e.messages))) + + # Set alerts + if import_done: + alert = f"{_('Part-Import')}
    {_('Imported {n} parts').format(n=import_done)}" + messages.success(self.request, alert) + if import_error: + error_text = '\n'.join([f'
  • x{import_error.count(a)}: {a}
  • ' for a in set(import_error)]) + messages.error(self.request, f"{_('Some errors occured:')}
      {error_text}
    ") return HttpResponseRedirect(reverse('part-index')) From 27ed20c1239f08cb356abd187ae30954f8ac099d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 May 2021 08:36:08 +0200 Subject: [PATCH 10/68] fix for wrong mapping --- InvenTree/part/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 3170a48c16..89eaca25c4 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -739,7 +739,7 @@ class PartImport(FileManagementFormView): 'name': 'name', 'description': 'description', 'keywords': 'keywords', - 'IPN': 'IPN', + 'ipn': 'ipn', 'revision': 'revision', 'link': 'link', 'default_expiry': 'default_expiry', From 941ac25d53350be3995e40fc04bae420a74fb00b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 May 2021 08:45:01 +0200 Subject: [PATCH 11/68] style fixes --- InvenTree/common/files.py | 1 - InvenTree/part/views.py | 1 - 2 files changed, 2 deletions(-) diff --git a/InvenTree/common/files.py b/InvenTree/common/files.py index c4b40e1e2b..9c4e7c039c 100644 --- a/InvenTree/common/files.py +++ b/InvenTree/common/files.py @@ -130,7 +130,6 @@ class FileManager: # Update headers self.update_headers() - 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/part/views.py b/InvenTree/part/views.py index 89eaca25c4..ac2749a612 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -763,7 +763,6 @@ class PartImport(FileManagementFormView): data = row['data'][idx_s[idx]]['cell'] row[idx.lower()] = data - def done(self, form_list, **kwargs): """ Create items """ items = {} From 90ae2813870588fcb2f10fb2dfb5baf69c6ac4b0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 May 2021 15:50:13 +0200 Subject: [PATCH 12/68] adding in optional headers --- InvenTree/common/files.py | 6 ++++-- InvenTree/common/forms.py | 21 +++++++++++++++++++++ InvenTree/part/views.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/InvenTree/common/files.py b/InvenTree/common/files.py index 9c4e7c039c..62f6426ef7 100644 --- a/InvenTree/common/files.py +++ b/InvenTree/common/files.py @@ -26,6 +26,8 @@ class FileManager: # Fields which would be helpful but are not required OPTIONAL_HEADERS = [] + OPTIONAL_MATCH_HEADERS = [] + EDITABLE_HEADERS = [] HEADERS = [] @@ -82,8 +84,8 @@ class FileManager: def update_headers(self): """ Update headers """ - self.HEADERS = self.REQUIRED_HEADERS + self.ITEM_MATCH_HEADERS + self.OPTIONAL_HEADERS - + self.HEADERS = self.REQUIRED_HEADERS + self.ITEM_MATCH_HEADERS + self.OPTIONAL_MATCH_HEADERS + self.OPTIONAL_HEADERS + def setup(self): """ Setup headers depending on the file name """ diff --git a/InvenTree/common/forms.py b/InvenTree/common/forms.py index c869d4a4f3..e166d235d8 100644 --- a/InvenTree/common/forms.py +++ b/InvenTree/common/forms.py @@ -220,3 +220,24 @@ class MatchItem(forms.Form): required=False, initial=value, ) + + # Optional item selection box + elif col_guess in file_manager.OPTIONAL_MATCH_HEADERS: + # Get item options + item_options = [(option.id, option) for option in row['match_options_' + col_guess]] + # Get item match + item_match = row['match_' + col_guess] + # Set field name + field_name = col_guess.lower() + '-' + str(row['index']) + # Set field select box + self.fields[field_name] = forms.ChoiceField( + choices=[('', '-' * 10)] + item_options, + required=False, + widget=forms.Select(attrs={ + 'class': 'select bomselect', + }) + ) + # Update select box when match was found + if item_match: + # Update initial value + self.fields[field_name].initial = item_match.id diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index ac2749a612..23799e4fec 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -751,6 +751,7 @@ class PartImport(FileManagementFormView): def get_field_selection(self): """ Fill the form fields for step 3 """ + self.file_manager.setup() # collect reference indexes idx_s = {} for col in self.file_manager.HEADERS: @@ -758,9 +759,24 @@ class PartImport(FileManagementFormView): if index >= 0: idx_s[col] = index + # fetch available elements + self.allowed_items = {} + self.matches = {} for row in self.rows: for idx in idx_s: data = row['data'][idx_s[idx]]['cell'] + + if idx in self.file_manager.OPTIONAL_MATCH_HEADERS: + try: + exact_match = self.allowed_items[idx].get(**{a:data for a in self.matches[idx]}) + except (ValueError, self.allowed_items[idx].model.DoesNotExist, self.allowed_items[idx].model.MultipleObjectsReturned): + exact_match = None + + row['match_options_' + idx] = self.allowed_items[idx] + row['match_' + idx] = exact_match + continue + + # general fields row[idx.lower()] = data def done(self, form_list, **kwargs): @@ -793,6 +809,19 @@ class PartImport(FileManagementFormView): # Create Part instances for part_data in items.values(): + + # set related parts + optional_matches = {} + for idx in self.file_manager.OPTIONAL_MATCH_HEADERS: + if idx.lower() in part_data: + try: + optional_matches[idx] = self.allowed_items[idx].get(pk=int(part_data[idx.lower()])) + except (ValueError, self.allowed_items[idx].model.DoesNotExist, self.allowed_items[idx].model.MultipleObjectsReturned): + optional_matches[idx] = None + else: + optional_matches[idx] = None + + # add part new_part = Part( name=part_data.get('name', ''), description=part_data.get('description', ''), From 8168db806180f71541fa845fee9fac868acab13d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 May 2021 15:57:57 +0200 Subject: [PATCH 13/68] implementation for part import --- InvenTree/common/files.py | 6 ++++++ InvenTree/part/views.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/InvenTree/common/files.py b/InvenTree/common/files.py index 62f6426ef7..94f96f42e5 100644 --- a/InvenTree/common/files.py +++ b/InvenTree/common/files.py @@ -118,6 +118,12 @@ class FileManager: 'Description', ] + self.OPTIONAL_MATCH_HEADERS = [ + 'Category', + 'default_location', + 'default_supplier', + ] + self.OPTIONAL_HEADERS = [ 'Keywords', 'IPN', diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 23799e4fec..75ab43ef81 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -42,6 +42,8 @@ from common.models import InvenTreeSetting from company.models import SupplierPart from common.views import FileManagementFormView +from stock.models import StockLocation + import common.settings as inventree_settings from . import forms as part_forms @@ -746,6 +748,9 @@ class PartImport(FileManagementFormView): 'minimum_stock': 'minimum_stock', 'units': 'units', 'notes': 'notes', + 'category': 'category', + 'default_location': 'default_location', + 'default_supplier': 'default_supplier', } def get_field_selection(self): @@ -762,6 +767,14 @@ class PartImport(FileManagementFormView): # fetch available elements self.allowed_items = {} self.matches = {} + + self.allowed_items['Category'] = PartCategory.objects.all() + self.matches['Category'] = ['name__contains'] + self.allowed_items['default_location'] = StockLocation.objects.all() + self.matches['default_location'] = ['name__contains'] + self.allowed_items['default_supplier'] = SupplierPart.objects.all() + self.matches['default_supplier'] = ['SKU__contains'] + for row in self.rows: for idx in idx_s: data = row['data'][idx_s[idx]]['cell'] @@ -833,6 +846,9 @@ class PartImport(FileManagementFormView): minimum_stock=part_data.get('minimum_stock', 0), units=part_data.get('units', None), notes=part_data.get('notes', None), + category=optional_matches['Category'], + default_location=optional_matches['default_location'], + default_supplier=optional_matches['default_supplier'], ) try: new_part.save() From b9c73b1e603d4fe7e6f882bf670a8411df244b2d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 May 2021 15:58:35 +0200 Subject: [PATCH 14/68] simpler code --- InvenTree/common/forms.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/InvenTree/common/forms.py b/InvenTree/common/forms.py index e166d235d8..0c91dcfeea 100644 --- a/InvenTree/common/forms.py +++ b/InvenTree/common/forms.py @@ -145,11 +145,11 @@ class MatchItem(forms.Form): for col in row['data']: # Get column matching col_guess = col['column'].get('guess', None) + # Set field name + field_name = col_guess.lower() + '-' + str(row['index']) # Create input for required headers if col_guess in file_manager.REQUIRED_HEADERS: - # Set field name - field_name = col_guess.lower() + '-' + str(row['index']) # Set field input box # TODO maybe not here but in an own function? @@ -201,8 +201,6 @@ class MatchItem(forms.Form): # Optional entries elif col_guess in file_manager.OPTIONAL_HEADERS: - # Set field name - field_name = col_guess.lower() + '-' + str(row['index']) # Get value value = row.get(col_guess.lower(), '') # Set field input box @@ -227,8 +225,6 @@ class MatchItem(forms.Form): item_options = [(option.id, option) for option in row['match_options_' + col_guess]] # Get item match item_match = row['match_' + col_guess] - # Set field name - field_name = col_guess.lower() + '-' + str(row['index']) # Set field select box self.fields[field_name] = forms.ChoiceField( choices=[('', '-' * 10)] + item_options, From 3a5b4ab74b6979a583ccd16323e40f895efe4298 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 May 2021 16:00:02 +0200 Subject: [PATCH 15/68] crispy forms for FileManagementFormView --- InvenTree/common/views.py | 10 ++++++++++ .../templates/order/order_wizard/match_parts.html | 9 +++++---- .../templates/part/import_wizard/match_references.html | 3 ++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/InvenTree/common/views.py b/InvenTree/common/views.py index fa605c2b80..0b2eda794e 100644 --- a/InvenTree/common/views.py +++ b/InvenTree/common/views.py @@ -13,6 +13,7 @@ from django.conf import settings from django.core.files.storage import FileSystemStorage from formtools.wizard.views import SessionWizardView +from crispy_forms.helper import FormHelper from InvenTree.views import AjaxUpdateView from InvenTree.helpers import str2bool @@ -269,6 +270,15 @@ class FileManagementFormView(MultiStepFormView): return super().get_form_kwargs() + def get_form(self, step=None, data=None, files=None): + """ add crispy-form helper to form """ + form = super().get_form(step=step, data=data, files=files) + + form.helper = FormHelper() + form.helper.form_show_labels = False + + return form + def get_form_table_data(self, form_data): """ Extract table cell data from form data and fields. These data are used to maintain state between sessions. diff --git a/InvenTree/order/templates/order/order_wizard/match_parts.html b/InvenTree/order/templates/order/order_wizard/match_parts.html index f97edff913..e0f030bad5 100644 --- a/InvenTree/order/templates/order/order_wizard/match_parts.html +++ b/InvenTree/order/templates/order/order_wizard/match_parts.html @@ -2,6 +2,7 @@ {% load inventree_extras %} {% load i18n %} {% load static %} +{% load crispy_forms_tags %} {% block form_alert %} {% if form.errors %} @@ -67,7 +68,7 @@ {% for field in form.visible_fields %} {% if field.name == row.quantity %} - {{ field }} + {{ field|as_crispy_field }} {% endif %} {% endfor %} {% if row.errors.quantity %} @@ -80,19 +81,19 @@ {% if item.column.guess == 'Purchase_Price' %} {% for field in form.visible_fields %} {% if field.name == row.purchase_price %} - {{ field }} + {{ field|as_crispy_field }} {% endif %} {% endfor %} {% elif item.column.guess == 'Reference' %} {% for field in form.visible_fields %} {% if field.name == row.reference %} - {{ field }} + {{ field|as_crispy_field }} {% endif %} {% endfor %} {% elif item.column.guess == 'Notes' %} {% for field in form.visible_fields %} {% if field.name == row.notes %} - {{ field }} + {{ field|as_crispy_field }} {% endif %} {% endfor %} {% else %} diff --git a/InvenTree/part/templates/part/import_wizard/match_references.html b/InvenTree/part/templates/part/import_wizard/match_references.html index efc69b98d5..99b9ccd191 100644 --- a/InvenTree/part/templates/part/import_wizard/match_references.html +++ b/InvenTree/part/templates/part/import_wizard/match_references.html @@ -2,6 +2,7 @@ {% load inventree_extras %} {% load i18n %} {% load static %} +{% load crispy_forms_tags %} {% block form_alert %} {% if form.errors %} @@ -57,7 +58,7 @@ {% with row_name=item.column.guess|lower %} {% for field in form.visible_fields %} {% if field.name == row|keyvalue:row_name %} - {{ field }} + {{ field|as_crispy_field }} {% endif %} {% endfor %} {% endwith %} From 508099e536360184f269bcd45b601f7598f168e9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 May 2021 19:53:01 +0200 Subject: [PATCH 16/68] style fixing --- InvenTree/common/views.py | 2 +- InvenTree/part/views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/common/views.py b/InvenTree/common/views.py index 0b2eda794e..6018f64cef 100644 --- a/InvenTree/common/views.py +++ b/InvenTree/common/views.py @@ -275,7 +275,7 @@ class FileManagementFormView(MultiStepFormView): form = super().get_form(step=step, data=data, files=files) form.helper = FormHelper() - form.helper.form_show_labels = False + form.helper.form_show_labels = False return form diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 75ab43ef81..266b31ada7 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -781,7 +781,7 @@ class PartImport(FileManagementFormView): if idx in self.file_manager.OPTIONAL_MATCH_HEADERS: try: - exact_match = self.allowed_items[idx].get(**{a:data for a in self.matches[idx]}) + exact_match = self.allowed_items[idx].get(**{a: data for a in self.matches[idx]}) except (ValueError, self.allowed_items[idx].model.DoesNotExist, self.allowed_items[idx].model.MultipleObjectsReturned): exact_match = None From eafaf92ae272691839cdd487f40bf9174db43941 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 18 May 2021 01:02:31 +0200 Subject: [PATCH 17/68] unified naming schema --- InvenTree/part/templates/part/category_navbar.html | 6 +++--- InvenTree/part/urls.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/InvenTree/part/templates/part/category_navbar.html b/InvenTree/part/templates/part/category_navbar.html index e5fb13bbda..e2cbaf55db 100644 --- a/InvenTree/part/templates/part/category_navbar.html +++ b/InvenTree/part/templates/part/category_navbar.html @@ -30,10 +30,10 @@
    -
  • - +
  • + - {% trans "Upload File" %} + {% trans "Import Parts" %}
  • diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index e2b172780a..131797ea03 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -121,7 +121,7 @@ part_urls = [ url(r'^new/?', views.PartCreate.as_view(), name='part-create'), # Upload a part - url(r'^import/', views.PartImport.as_view(), name='part-upload'), + url(r'^import/', views.PartImport.as_view(), name='part-import'), # Create a new BOM item url(r'^bom/new/?', views.BomItemCreate.as_view(), name='bom-item-create'), From 92f8bd36f1eb962ead461de29ac87e6a8d9cd282 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 18 May 2021 01:55:47 +0200 Subject: [PATCH 18/68] inherited setup method --- InvenTree/common/files.py | 52 +++++---------------------------------- InvenTree/common/views.py | 11 ++++++++- InvenTree/order/views.py | 21 ++++++++++++++++ InvenTree/part/views.py | 28 +++++++++++++++++++++ 4 files changed, 65 insertions(+), 47 deletions(-) diff --git a/InvenTree/common/files.py b/InvenTree/common/files.py index 94f96f42e5..52e461c9c7 100644 --- a/InvenTree/common/files.py +++ b/InvenTree/common/files.py @@ -87,56 +87,16 @@ class FileManager: self.HEADERS = self.REQUIRED_HEADERS + self.ITEM_MATCH_HEADERS + self.OPTIONAL_MATCH_HEADERS + self.OPTIONAL_HEADERS def setup(self): - """ Setup headers depending on the file name """ + """ + Setup headers + should be overriden in usage to set the Different Headers + """ if not self.name: return - if self.name == 'order': - self.REQUIRED_HEADERS = [ - 'Quantity', - ] - - self.ITEM_MATCH_HEADERS = [ - 'Manufacturer_MPN', - 'Supplier_SKU', - ] - - self.OPTIONAL_HEADERS = [ - 'Purchase_Price', - 'Reference', - 'Notes', - ] - - # Update headers - self.update_headers() - - # TODO maybe not here but in an own function? - if self.name == 'part': - self.REQUIRED_HEADERS = [ - 'Name', - 'Description', - ] - - self.OPTIONAL_MATCH_HEADERS = [ - 'Category', - 'default_location', - 'default_supplier', - ] - - self.OPTIONAL_HEADERS = [ - 'Keywords', - 'IPN', - 'Revision', - 'Link', - 'default_expiry', - 'minimum_stock', - 'Units', - 'Notes', - ] - - # Update headers - self.update_headers() + # Update headers + self.update_headers() 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 669a5e8212..72a8d7fb1a 100644 --- a/InvenTree/common/views.py +++ b/InvenTree/common/views.py @@ -189,6 +189,15 @@ class FileManagementFormView(MultiStepFormView): media_folder = 'file_upload/' extra_context_data = {} + def __init__(self, *args, **kwargs): + """ initialize the FormView """ + # perform all checks and inits from MultiStepFormView + super().__init__(*args, **kwargs) + + # Check + if not(hasattr(self, 'file_manager_class') and issubclass(self.file_manager_class, FileManager)): + raise NotImplementedError('A subclass of a file manager class needs to be set!') + def get_context_data(self, form, **kwargs): context = super().get_context_data(form=form, **kwargs) @@ -228,7 +237,7 @@ class FileManagementFormView(MultiStepFormView): # Get file file = upload_files.get('upload-file', None) if file: - self.file_manager = FileManager(file=file, name=self.name) + self.file_manager = self.file_manager_class(file=file, name=self.name) def get_form_kwargs(self, step=None): """ Update kwargs to dynamically build forms """ diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index c8ec42d3e7..bbda2f1716 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -31,6 +31,7 @@ from part.models import Part from common.models import InvenTreeSetting from common.views import FileManagementFormView +from common.files import FileManager from . import forms as order_forms from part.views import PartPricing @@ -591,6 +592,26 @@ class PurchaseOrderUpload(FileManagementFormView): 'reference': 'reference', 'notes': 'notes', } + class MyManger(FileManager): + def setup(self): + self.REQUIRED_HEADERS = [ + 'Quantity', + ] + + self.ITEM_MATCH_HEADERS = [ + 'Manufacturer_MPN', + 'Supplier_SKU', + ] + + self.OPTIONAL_HEADERS = [ + 'Purchase_Price', + 'Reference', + 'Notes', + ] + + return super().setup() + + file_manager_class = MyManger def get_order(self): """ Get order or return 404 """ diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 266b31ada7..d3c32bebca 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -40,6 +40,7 @@ from .models import PartSellPriceBreak from common.models import InvenTreeSetting from company.models import SupplierPart +from common.files import FileManager from common.views import FileManagementFormView from stock.models import StockLocation @@ -752,6 +753,33 @@ class PartImport(FileManagementFormView): 'default_location': 'default_location', 'default_supplier': 'default_supplier', } + class MyManger(FileManager): + def setup(self): + self.REQUIRED_HEADERS = [ + 'Name', + 'Description', + ] + + self.OPTIONAL_MATCH_HEADERS = [ + 'Category', + 'default_location', + 'default_supplier', + ] + + self.OPTIONAL_HEADERS = [ + 'Keywords', + 'IPN', + 'Revision', + 'Link', + 'default_expiry', + 'minimum_stock', + 'Units', + 'Notes', + ] + + return super().setup() + + file_manager_class = MyManger def get_field_selection(self): """ Fill the form fields for step 3 """ From cb0ef30effafd7bf9319ae3bae3f1af2e2163b2e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 18 May 2021 10:17:19 +0200 Subject: [PATCH 19/68] form overrides --- InvenTree/common/views.py | 21 +++++++++++++++++++-- InvenTree/order/views.py | 11 ++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/InvenTree/common/views.py b/InvenTree/common/views.py index 72a8d7fb1a..b645d16651 100644 --- a/InvenTree/common/views.py +++ b/InvenTree/common/views.py @@ -191,10 +191,27 @@ class FileManagementFormView(MultiStepFormView): def __init__(self, *args, **kwargs): """ initialize the FormView """ - # perform all checks and inits from MultiStepFormView + # check if form_list should be overriden + if hasattr(self, 'form_list_override'): + # check for list + if not isinstance(self.form_list_override, list): + raise ValueError('form_list_override must be a list') + + # loop through and override /add form_list enrties + for entry in self.form_list_override: + # fetch postition + pos = [self.form_list.index(i) for i in self.form_list if i[0] == 'items'] + # replace if exists + if pos: + self.form_list[pos[0]] = entry + # or append + else: + self.form_list.append(entry) + + # perform all checks and inits for MultiStepFormView super().__init__(*args, **kwargs) - # Check + # Check for file manager class if not(hasattr(self, 'file_manager_class') and issubclass(self.file_manager_class, FileManager)): raise NotImplementedError('A subclass of a file manager class needs to be set!') diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index bbda2f1716..2cb7263269 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -32,6 +32,7 @@ from part.models import Part from common.models import InvenTreeSetting from common.views import FileManagementFormView from common.files import FileManager +from common import forms as cm_forms from . import forms as order_forms from part.views import PartPricing @@ -573,7 +574,16 @@ class SalesOrderShip(AjaxUpdateView): class PurchaseOrderUpload(FileManagementFormView): ''' PurchaseOrder: Upload file, match to fields and parts (using multi-Step form) ''' + class MyMatch(cm_forms.MatchItem): + """ override MatchItem fields """ + def get_special_field(self, col_guess, row, file_manager): + """ set special field """ + # run default + super().get_special_field(col_guess, row, file_manager) name = 'order' + form_list_override = [ + ('items', MyMatch), + ] form_steps_template = [ 'order/order_wizard/po_upload.html', 'order/order_wizard/match_fields.html', @@ -584,7 +594,6 @@ class PurchaseOrderUpload(FileManagementFormView): _("Match Fields"), _("Match Supplier Parts"), ] - # Form field name: PurchaseOrderLineItem field form_field_map = { 'item_select': 'part', 'quantity': 'quantity', From ad4902ea44e63436c9d9080ae42354646503917c Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 18 May 2021 10:19:35 +0200 Subject: [PATCH 20/68] restucture --- InvenTree/common/forms.py | 90 +++++++++++++++------------------------ InvenTree/order/views.py | 31 +++++++++++++- 2 files changed, 65 insertions(+), 56 deletions(-) diff --git a/InvenTree/common/forms.py b/InvenTree/common/forms.py index 0c91dcfeea..3ce83ac72f 100644 --- a/InvenTree/common/forms.py +++ b/InvenTree/common/forms.py @@ -10,8 +10,6 @@ from decimal import Decimal, InvalidOperation from django import forms from django.utils.translation import gettext as _ -from djmoney.forms.fields import MoneyField - from InvenTree.forms import HelperForm from .files import FileManager @@ -119,21 +117,6 @@ class MatchItem(forms.Form): super().__init__(*args, **kwargs) - def clean(number): - """ Clean-up decimal value """ - - # Check if empty - if not number: - return number - - # Check if decimal type - try: - clean_number = Decimal(number) - except InvalidOperation: - clean_number = number - - return clean_number.quantize(Decimal(1)) if clean_number == clean_number.to_integral() else clean_number.normalize() - # Setup FileManager file_manager.setup() @@ -148,32 +131,20 @@ class MatchItem(forms.Form): # Set field name field_name = col_guess.lower() + '-' + str(row['index']) + # check if field def was overriden + overriden_field = self.get_special_field(col_guess, row, file_manager) + if overriden_field: + self.fields[field_name] = overriden_field + # Create input for required headers - if col_guess in file_manager.REQUIRED_HEADERS: + elif col_guess in file_manager.REQUIRED_HEADERS: + # Get value + value = row.get(col_guess.lower(), '') # Set field input box - - # TODO maybe not here but in an own function? - if 'quantity' in col_guess.lower(): - self.fields[field_name] = forms.CharField( - required=False, - widget=forms.NumberInput(attrs={ - 'name': 'quantity' + str(row['index']), - 'class': 'numberinput', # form-control', - 'type': 'number', - 'min': '0', - 'step': 'any', - 'value': clean(row.get('quantity', '')), - }) - ) - - else: - # Get value - value = row.get(col_guess.lower(), '') - # Set field input box - self.fields[field_name] = forms.CharField( - required=True, - initial=value, - ) + self.fields[field_name] = forms.CharField( + required=True, + initial=value, + ) # Create item selection box elif col_guess in file_manager.ITEM_MATCH_HEADERS: @@ -204,20 +175,10 @@ class MatchItem(forms.Form): # Get value value = row.get(col_guess.lower(), '') # Set field input box - if 'price' in col_guess.lower(): - self.fields[field_name] = MoneyField( - label=_(col_guess), - default_currency=InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY'), - decimal_places=5, - max_digits=19, - required=False, - default_amount=clean(value), - ) - else: - self.fields[field_name] = forms.CharField( - required=False, - initial=value, - ) + self.fields[field_name] = forms.CharField( + required=False, + initial=value, + ) # Optional item selection box elif col_guess in file_manager.OPTIONAL_MATCH_HEADERS: @@ -237,3 +198,22 @@ class MatchItem(forms.Form): if item_match: # Update initial value self.fields[field_name].initial = item_match.id + + def clean_nbr(self, number): + """ Clean-up decimal value """ + + # Check if empty + if not number: + return number + + # Check if decimal type + try: + clean_number = Decimal(number) + except InvalidOperation: + clean_number = number + + return clean_number.quantize(Decimal(1)) if clean_number == clean_number.to_integral() else clean_number.normalize() + + def get_special_field(self, col_guess, row, file_manager): + """ function to be overriden in inherited forms to add specific form settings """ + pass diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 2cb7263269..d89401e3cb 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -15,7 +15,7 @@ from django.http import HttpResponseRedirect from django.utils.translation import ugettext_lazy as _ from django.views.generic import DetailView, ListView, UpdateView from django.views.generic.edit import FormMixin -from django.forms import HiddenInput, IntegerField +from django.forms import HiddenInput, IntegerField, CharField, NumberInput import logging from decimal import Decimal, InvalidOperation @@ -44,6 +44,8 @@ from InvenTree.views import InvenTreeRoleMixin from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus +from djmoney.forms.fields import MoneyField + logger = logging.getLogger("inventree") @@ -580,6 +582,33 @@ class PurchaseOrderUpload(FileManagementFormView): """ set special field """ # run default super().get_special_field(col_guess, row, file_manager) + + # set quantity field + if 'quantity' in col_guess.lower(): + return CharField( + required=False, + widget=NumberInput(attrs={ + 'name': 'quantity' + str(row['index']), + 'class': 'numberinput', + 'type': 'number', + 'min': '0', + 'step': 'any', + 'value': self.clean_nbr(row.get('quantity', '')), + }) + ) + # set price field + elif 'price' in col_guess.lower(): + return MoneyField( + label=_(col_guess), + default_currency=InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY'), + decimal_places=5, + max_digits=19, + required=False, + default_amount=self.clean_nbr(row.get('price', '')), + ) + + + name = 'order' form_list_override = [ ('items', MyMatch), From 64f8846e9901bfc0f849049c7c06e16c5a64772c Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 18 May 2021 10:47:56 +0200 Subject: [PATCH 21/68] generalising for done() --- InvenTree/common/views.py | 27 +++++++++++++++++++++++++++ InvenTree/order/views.py | 21 +-------------------- InvenTree/part/views.py | 23 +---------------------- 3 files changed, 29 insertions(+), 42 deletions(-) diff --git a/InvenTree/common/views.py b/InvenTree/common/views.py index b645d16651..53909cc0d7 100644 --- a/InvenTree/common/views.py +++ b/InvenTree/common/views.py @@ -450,6 +450,33 @@ class FileManagementFormView(MultiStepFormView): """ pass + def get_clean_items(self): + """ returns dict with all cleaned values """ + items = {} + + for form_key, form_value in self.get_all_cleaned_data().items(): + # Split key from row value + try: + (field, idx) = form_key.split('-') + except ValueError: + continue + + try: + if idx not in items: + # Insert into items + items.update({ + idx: { + self.form_field_map[field]: form_value, + } + }) + else: + # Update items + items[idx][self.form_field_map[field]] = form_value + except KeyError: + pass + + return items + def check_field_selection(self, form): """ Check field matching """ diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index d89401e3cb..3db0d4412f 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -767,26 +767,7 @@ class PurchaseOrderUpload(FileManagementFormView): """ Once all the data is in, process it to add PurchaseOrderLineItem instances to the order """ order = self.get_order() - - items = {} - - for form_key, form_value in self.get_all_cleaned_data().items(): - # Split key from row value - try: - (field, idx) = form_key.split('-') - except ValueError: - continue - - if idx not in items: - # Insert into items - items.update({ - idx: { - self.form_field_map[field]: form_value, - } - }) - else: - # Update items - items[idx][self.form_field_map[field]] = form_value + items = self.get_clean_items() # Create PurchaseOrderLineItem instances for purchase_order_item in items.values(): diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index d3c32bebca..1e5e2153e8 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -822,28 +822,7 @@ class PartImport(FileManagementFormView): def done(self, form_list, **kwargs): """ Create items """ - items = {} - - for form_key, form_value in self.get_all_cleaned_data().items(): - # Split key from row value - try: - (field, idx) = form_key.split('-') - except ValueError: - continue - - try: - if idx not in items: - # Insert into items - items.update({ - idx: { - self.form_field_map[field]: form_value, - } - }) - else: - # Update items - items[idx][self.form_field_map[field]] = form_value - except KeyError: - pass + items = self.get_clean_items() import_done = 0 import_error = [] From db9fd282768f1bacc89a68710b8cae380583b0db Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 18 May 2021 10:48:29 +0200 Subject: [PATCH 22/68] preparing stuff for gen get_field_selection() --- InvenTree/part/views.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 1e5e2153e8..45a5ad1c2b 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -783,15 +783,6 @@ class PartImport(FileManagementFormView): def get_field_selection(self): """ Fill the form fields for step 3 """ - - self.file_manager.setup() - # collect reference indexes - idx_s = {} - for col in self.file_manager.HEADERS: - index = self.get_column_index(col) - if index >= 0: - idx_s[col] = index - # fetch available elements self.allowed_items = {} self.matches = {} @@ -803,6 +794,15 @@ class PartImport(FileManagementFormView): self.allowed_items['default_supplier'] = SupplierPart.objects.all() self.matches['default_supplier'] = ['SKU__contains'] + # setup + self.file_manager.setup() + # collect reference indexes + idx_s = {} + for col in self.file_manager.HEADERS: + index = self.get_column_index(col) + if index >= 0: + idx_s[col] = index + for row in self.rows: for idx in idx_s: data = row['data'][idx_s[idx]]['cell'] From e49256a2182fe1d1b8e57958c398ce7549688641 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 18 May 2021 10:52:27 +0200 Subject: [PATCH 23/68] fixed bug pointed out by @eeintech --- InvenTree/part/templates/part/import_wizard/match_fields.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/part/templates/part/import_wizard/match_fields.html b/InvenTree/part/templates/part/import_wizard/match_fields.html index 54008d6bae..ba709bc639 100644 --- a/InvenTree/part/templates/part/import_wizard/match_fields.html +++ b/InvenTree/part/templates/part/import_wizard/match_fields.html @@ -55,7 +55,7 @@ {{ col }} {% for duplicate in duplicates %} - {% if duplicate == col.name %} + {% if duplicate == col.value %} From 616dd76f8ab2ef9993b73ca39d833cd403b0a38d Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 18 May 2021 11:10:01 +0200 Subject: [PATCH 24/68] refactor and doc --- InvenTree/order/views.py | 39 +++++++++++++++++++-------------------- InvenTree/part/views.py | 12 +++++++----- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 3db0d4412f..9aa9b8d473 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -576,6 +576,7 @@ class SalesOrderShip(AjaxUpdateView): class PurchaseOrderUpload(FileManagementFormView): ''' PurchaseOrder: Upload file, match to fields and parts (using multi-Step form) ''' + # overriden classes class MyMatch(cm_forms.MatchItem): """ override MatchItem fields """ def get_special_field(self, col_guess, row, file_manager): @@ -607,7 +608,24 @@ class PurchaseOrderUpload(FileManagementFormView): default_amount=self.clean_nbr(row.get('price', '')), ) + class MyFileManager(FileManager): + def setup(self): + self.REQUIRED_HEADERS = [ + 'Quantity', + ] + self.ITEM_MATCH_HEADERS = [ + 'Manufacturer_MPN', + 'Supplier_SKU', + ] + + self.OPTIONAL_HEADERS = [ + 'Purchase_Price', + 'Reference', + 'Notes', + ] + + return super().setup() name = 'order' form_list_override = [ @@ -630,26 +648,7 @@ class PurchaseOrderUpload(FileManagementFormView): 'reference': 'reference', 'notes': 'notes', } - class MyManger(FileManager): - def setup(self): - self.REQUIRED_HEADERS = [ - 'Quantity', - ] - - self.ITEM_MATCH_HEADERS = [ - 'Manufacturer_MPN', - 'Supplier_SKU', - ] - - self.OPTIONAL_HEADERS = [ - 'Purchase_Price', - 'Reference', - 'Notes', - ] - - return super().setup() - - file_manager_class = MyManger + file_manager_class = MyFileManager def get_order(self): """ Get order or return 404 """ diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 45a5ad1c2b..65a5cc652d 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -796,16 +796,18 @@ class PartImport(FileManagementFormView): # setup self.file_manager.setup() - # collect reference indexes - idx_s = {} + # collect submitted column indexes + col_ids = {} for col in self.file_manager.HEADERS: index = self.get_column_index(col) if index >= 0: - idx_s[col] = index + col_ids[col] = index + # parse all rows for row in self.rows: - for idx in idx_s: - data = row['data'][idx_s[idx]]['cell'] + # check each submitted column + for idx in col_ids: + data = row['data'][col_ids[idx]]['cell'] if idx in self.file_manager.OPTIONAL_MATCH_HEADERS: try: From dd56bc1fa531eca2a6e703443d9d3540e2eca4aa Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 18 May 2021 11:18:13 +0200 Subject: [PATCH 25/68] setup not realy needed --- InvenTree/order/views.py | 27 ++++++++++++-------------- InvenTree/part/views.py | 42 ++++++++++++++++++---------------------- 2 files changed, 31 insertions(+), 38 deletions(-) diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 9aa9b8d473..5d0acb9a7c 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -609,23 +609,20 @@ class PurchaseOrderUpload(FileManagementFormView): ) class MyFileManager(FileManager): - def setup(self): - self.REQUIRED_HEADERS = [ - 'Quantity', - ] + REQUIRED_HEADERS = [ + 'Quantity', + ] - self.ITEM_MATCH_HEADERS = [ - 'Manufacturer_MPN', - 'Supplier_SKU', - ] + ITEM_MATCH_HEADERS = [ + 'Manufacturer_MPN', + 'Supplier_SKU', + ] - self.OPTIONAL_HEADERS = [ - 'Purchase_Price', - 'Reference', - 'Notes', - ] - - return super().setup() + OPTIONAL_HEADERS = [ + 'Purchase_Price', + 'Reference', + 'Notes', + ] name = 'order' form_list_override = [ diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 65a5cc652d..1a792dd0ce 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -754,31 +754,27 @@ class PartImport(FileManagementFormView): 'default_supplier': 'default_supplier', } class MyManger(FileManager): - def setup(self): - self.REQUIRED_HEADERS = [ - 'Name', - 'Description', - ] + REQUIRED_HEADERS = [ + 'Name', + 'Description', + ] - self.OPTIONAL_MATCH_HEADERS = [ - 'Category', - 'default_location', - 'default_supplier', - ] - - self.OPTIONAL_HEADERS = [ - 'Keywords', - 'IPN', - 'Revision', - 'Link', - 'default_expiry', - 'minimum_stock', - 'Units', - 'Notes', - ] - - return super().setup() + OPTIONAL_MATCH_HEADERS = [ + 'Category', + 'default_location', + 'default_supplier', + ] + OPTIONAL_HEADERS = [ + 'Keywords', + 'IPN', + 'Revision', + 'Link', + 'default_expiry', + 'minimum_stock', + 'Units', + 'Notes', + ] file_manager_class = MyManger def get_field_selection(self): From 9a42421852b0fe69264b25cac4a2a598283ceaa4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 18 May 2021 11:20:25 +0200 Subject: [PATCH 26/68] restructure overrides --- InvenTree/part/views.py | 47 +++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 1a792dd0ce..b9c4d2ec03 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -726,6 +726,29 @@ class PartCreate(AjaxCreateView): class PartImport(FileManagementFormView): ''' Part: Upload file, match to fields and import parts(using multi-Step form) ''' + class MyFileManager(FileManager): + REQUIRED_HEADERS = [ + 'Name', + 'Description', + ] + + OPTIONAL_MATCH_HEADERS = [ + 'Category', + 'default_location', + 'default_supplier', + ] + + OPTIONAL_HEADERS = [ + 'Keywords', + 'IPN', + 'Revision', + 'Link', + 'default_expiry', + 'minimum_stock', + 'Units', + 'Notes', + ] + name = 'part' form_steps_template = [ 'part/import_wizard/part_upload.html', @@ -753,29 +776,7 @@ class PartImport(FileManagementFormView): 'default_location': 'default_location', 'default_supplier': 'default_supplier', } - class MyManger(FileManager): - REQUIRED_HEADERS = [ - 'Name', - 'Description', - ] - - OPTIONAL_MATCH_HEADERS = [ - 'Category', - 'default_location', - 'default_supplier', - ] - - OPTIONAL_HEADERS = [ - 'Keywords', - 'IPN', - 'Revision', - 'Link', - 'default_expiry', - 'minimum_stock', - 'Units', - 'Notes', - ] - file_manager_class = MyManger + file_manager_class = MyFileManager def get_field_selection(self): """ Fill the form fields for step 3 """ From 900f707ff9cebc1e7da9fe3e6fa43f310b307552 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 18 May 2021 11:51:08 +0200 Subject: [PATCH 27/68] permission added --- InvenTree/part/templates/part/category_navbar.html | 2 ++ InvenTree/part/views.py | 1 + 2 files changed, 3 insertions(+) diff --git a/InvenTree/part/templates/part/category_navbar.html b/InvenTree/part/templates/part/category_navbar.html index e2cbaf55db..553b03745b 100644 --- a/InvenTree/part/templates/part/category_navbar.html +++ b/InvenTree/part/templates/part/category_navbar.html @@ -30,12 +30,14 @@ + {% if user.is_staff and roles.part.add %}
  • {% trans "Import Parts" %}
  • + {% endif %} {% if category %}
  • diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index b9c4d2ec03..20185b7a1c 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -725,6 +725,7 @@ class PartCreate(AjaxCreateView): class PartImport(FileManagementFormView): ''' Part: Upload file, match to fields and import parts(using multi-Step form) ''' + permission_required = 'part.add' class MyFileManager(FileManager): REQUIRED_HEADERS = [ From 3c5bb048a1b8e3bd964dc85973f20c5cb7f71312 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 May 2021 15:39:54 +0200 Subject: [PATCH 28/68] renaming a few parts --- InvenTree/order/views.py | 8 ++++---- InvenTree/part/views.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 5d0acb9a7c..312c49b6db 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -577,7 +577,7 @@ class PurchaseOrderUpload(FileManagementFormView): ''' PurchaseOrder: Upload file, match to fields and parts (using multi-Step form) ''' # overriden classes - class MyMatch(cm_forms.MatchItem): + class OrderMatchItem(cm_forms.MatchItem): """ override MatchItem fields """ def get_special_field(self, col_guess, row, file_manager): """ set special field """ @@ -608,7 +608,7 @@ class PurchaseOrderUpload(FileManagementFormView): default_amount=self.clean_nbr(row.get('price', '')), ) - class MyFileManager(FileManager): + class OrderFileManager(FileManager): REQUIRED_HEADERS = [ 'Quantity', ] @@ -626,7 +626,7 @@ class PurchaseOrderUpload(FileManagementFormView): name = 'order' form_list_override = [ - ('items', MyMatch), + ('items', OrderMatchItem), ] form_steps_template = [ 'order/order_wizard/po_upload.html', @@ -645,7 +645,7 @@ class PurchaseOrderUpload(FileManagementFormView): 'reference': 'reference', 'notes': 'notes', } - file_manager_class = MyFileManager + file_manager_class = OrderFileManager def get_order(self): """ Get order or return 404 """ diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 20185b7a1c..91ff3bacf6 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -727,7 +727,7 @@ class PartImport(FileManagementFormView): ''' Part: Upload file, match to fields and import parts(using multi-Step form) ''' permission_required = 'part.add' - class MyFileManager(FileManager): + class PartFileManager(FileManager): REQUIRED_HEADERS = [ 'Name', 'Description', @@ -777,7 +777,7 @@ class PartImport(FileManagementFormView): 'default_location': 'default_location', 'default_supplier': 'default_supplier', } - file_manager_class = MyFileManager + file_manager_class = PartFileManager def get_field_selection(self): """ Fill the form fields for step 3 """ From 4319ba16af0fc9dfe96fa6322bc596fd21f5eea7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 26 May 2021 09:20:50 +0200 Subject: [PATCH 29/68] Settings to show import-button --- InvenTree/common/models.py | 7 +++++++ .../part/templates/part/category_navbar.html | 5 ++++- .../templates/InvenTree/settings/part.html | 20 +++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 99712b2a93..30de70f6d9 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -211,6 +211,13 @@ class InvenTreeSetting(models.Model): 'validator': bool, }, + 'PART_SHOW_IMPORT': { + 'name': _('Show Import in Views'), + 'description': _('Display the import wizard in some part views'), + 'default': False, + 'validator': bool, + }, + 'REPORT_DEBUG_MODE': { 'name': _('Debug Mode'), 'description': _('Generate reports in debug mode (HTML output)'), diff --git a/InvenTree/part/templates/part/category_navbar.html b/InvenTree/part/templates/part/category_navbar.html index 553b03745b..f6d083b864 100644 --- a/InvenTree/part/templates/part/category_navbar.html +++ b/InvenTree/part/templates/part/category_navbar.html @@ -1,4 +1,7 @@ {% load i18n %} +{% load inventree_extras %} + +{% settings_value 'PART_SHOW_IMPORT' as show_import %}