Added match parts step, need to process fields data

This commit is contained in:
eeintech 2021-05-04 14:45:52 -04:00
parent 7cdf0af04a
commit 64fb492b97
7 changed files with 326 additions and 36 deletions

View File

@ -18,25 +18,17 @@ class FileManager:
name = '' name = ''
# Fields which are absolutely necessary for valid upload # Fields which are absolutely necessary for valid upload
REQUIRED_HEADERS = [ REQUIRED_HEADERS = []
'Quantity'
]
# Fields which are used for part matching (only one of them is needed) # Fields which are used for part matching (only one of them is needed)
PART_MATCH_HEADERS = [ PART_MATCH_HEADERS = []
'Part_Name',
'Part_IPN',
'Part_ID',
]
# Fields which would be helpful but are not required # 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): def __init__(self, file, name=None):
""" Initialize the FileManager class with a user-uploaded file object """ """ Initialize the FileManager class with a user-uploaded file object """
@ -48,6 +40,9 @@ class FileManager:
# Process initial file # Process initial file
self.process(file) self.process(file)
# Update headers
self.update_headers()
def process(self, file): def process(self, file):
""" Process file """ """ Process file """
@ -69,6 +64,37 @@ class FileManager:
raise ValidationError(_(f'Error reading {self.name} file (invalid format)')) raise ValidationError(_(f'Error reading {self.name} file (invalid format)'))
except tablib.core.InvalidDimensions: except tablib.core.InvalidDimensions:
raise ValidationError(_(f'Error reading {self.name} file (incorrect dimension)')) 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): def guess_header(self, header, threshold=80):
""" Try to match a header (from the file) to a list of known headers """ Try to match a header (from the file) to a list of known headers

View File

@ -117,22 +117,45 @@ class MultiStepFormView(SessionWizardView):
""" """
form_list = [] form_list = []
form_steps_template = []
form_steps_description = [] form_steps_description = []
file_manager = None
media_folder = '' media_folder = ''
file_storage = FileSystemStorage(settings.MEDIA_ROOT) file_storage = FileSystemStorage(settings.MEDIA_ROOT)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
""" Override init method to set media folder """
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.process_media_folder()
def process_media_folder(self):
""" Process media folder """
if self.media_folder: if self.media_folder:
media_folder_abs = os.path.join(settings.MEDIA_ROOT, self.media_folder) media_folder_abs = os.path.join(settings.MEDIA_ROOT, self.media_folder)
if not os.path.exists(media_folder_abs): if not os.path.exists(media_folder_abs):
os.mkdir(media_folder_abs) os.mkdir(media_folder_abs)
self.file_storage = FileSystemStorage(location=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): def get_context_data(self, **kwargs):
""" Update context data """
# Retrieve current context
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
# Get description
# Get form description
try: try:
description = self.form_steps_description[int(self.steps.current)] description = self.form_steps_description[int(self.steps.current)]
except IndexError: except IndexError:

View File

@ -309,9 +309,9 @@ class UploadFile(forms.Form):
class MatchField(forms.Form): class MatchField(forms.Form):
""" Step 2 """ """ Step 2 """
last_name = forms.CharField(max_length=100) pass
class MatchPart(forms.Form): class MatchPart(forms.Form):
""" Step 3 """ """ Step 3 """
age = forms.IntegerField() pass

View File

@ -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 %}
<div class='alert alert-danger alert-block' role='alert'>
{% trans "Missing selections for the following required columns" %}:
<br>
<ul>
{% for col in missing_columns %}
<li>{{ col }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<form method="post" action='' class='js-modal-form' enctype="multipart/form-data">
{% csrf_token %}
{% load crispy_forms_tags %}
{{ wizard.management_form }}
{% if wizard.steps.prev %}
<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}" class="save btn btn-default">{% trans "Previous Step" %}</button>
{% endif %}
<button type="submit" class="save btn btn-default">{% trans "Submit Selections" %}</button>
<table class='table table-striped' style='margin-top: 12px'>
<thead>
<tr>
<th></th>
<th>{% trans "File Fields" %}</th>
{% for col in columns %}
<th>
<div>
<input type='hidden' name='col_name_{{ forloop.counter0 }}' value='{{ col.name }}'/>
{{ col.name }}
<button class='btn btn-default btn-remove' onClick='removeColFromBomWizard()' id='del_col_{{ forloop.counter0 }}' style='display: inline; float: right;' title='{% trans "Remove column" %}'>
<span col_id='{{ forloop.counter0 }}' class='fas fa-trash-alt icon-red'></span>
</button>
</div>
</th>
{% endfor %}
</tr>
</thead>
<tbody>
<tr>
<td></td>
<td>{% trans "Match Fields" %}</td>
{% for col in columns %}
<td>
<select class='select' id='id_col_{{ forloop.counter0 }}' name='col_guess_{{ forloop.counter0 }}'>
<option value=''>---------</option>
{% for req in headers %}
<option value='{{ req }}'{% if req == col.guess %}selected='selected'{% endif %}>{{ req }}</option>
{% endfor %}
</select>
{% if col.duplicate %}
<p class='help-inline'>{% trans "Duplicate column selection" %}</p>
{% endif %}
</td>
{% endfor %}
</tr>
{% for row in rows %}
{% with forloop.counter as row_index %}
<tr>
<td>
<button class='btn btn-default btn-remove' onClick='removeRowFromBomWizard()' id='del_row_{{ row_index }}' style='display: inline; float: right;' title='{% trans "Remove row" %}'>
<span row_id='{{ row_index }}' class='fas fa-trash-alt icon-red'></span>
</button>
</td>
<td>{{ row_index }}</td>
{% for item in row.data %}
<td>
<input type='hidden' name='row_{{ row_index }}_col_{{ forloop.counter0 }}' value='{{ item }}'/>
{{ item }}
</td>
{% endfor %}
</tr>
{% endwith %}
{% endfor %}
</tbody>
</table>
</form>
{% endblock %}

View File

@ -0,0 +1,112 @@
{% extends "order/order_wizard/po_upload.html" %}
{% load inventree_extras %}
{% load i18n %}
{% load static %}
{% block form %}
{% if form_errors %}
<div class='alert alert-danger alert-block' role='alert'>
{% trans "Errors exist in the submitted data" %}
</div>
{% endif %}
<form method="post" action='' class='js-modal-form' enctype="multipart/form-data">
{% csrf_token %}
{% load crispy_forms_tags %}
{{ wizard.management_form }}
{% if wizard.steps.prev %}
<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}" class="save btn btn-default">{% trans "Previous Step" %}</button>
{% endif %}
<button type="submit" class="save btn btn-default">{% trans "Submit Selections" %}</button>
{% csrf_token %}
<table class='table table-striped'>
<thead>
<tr>
<th></th>
<th></th>
<th>{% trans "Row" %}</th>
<th>{% trans "Select Part" %}</th>
{% for col in bom_columns %}
<th>
<input type='hidden' name='col_name_{{ forloop.counter0 }}' value='{{ col.name }}'/>
<input type='hidden' name='col_guess_{{ forloop.counter0 }}' value='{{ col.guess }}'/>
{% if col.guess %}
{{ col.guess }}
{% else %}
{{ col.name }}
{% endif %}
</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in bom_rows %}
<tr {% if row.errors %} style='background: #ffeaea;'{% endif %} part-name='{{ row.part_name }}' part-description='{{ row.description }}' part-select='#select_part_{{ row.index }}'>
<td>
<button class='btn btn-default btn-remove' onClick='removeRowFromBomWizard()' id='del_row_{{ forloop.counter }}' style='display: inline; float: right;' title='{% trans "Remove row" %}'>
<span row_id='{{ forloop.counter }}' class='fas fa-trash-alt icon-red'></span>
</button>
</td>
<td></td>
<td>{% add row.index 1 %}</td>
<td>
<button class='btn btn-default btn-create' onClick='newPartFromBomWizard()' id='new_part_row_{{ row.index }}' title='{% trans "Create new part" %}' type='button'>
<span row_id='{{ row.index }}' class='fas fa-plus icon-green'/>
</button>
<select class='select bomselect' id='select_part_{{ row.index }}' name='part_{{ row.index }}'>
<option value=''>--- {% trans "Select Part" %} ---</option>
{% for part in row.part_options %}
<option value='{{ part.id }}' {% if part.id == row.part.id %} selected='selected' {% elif part.id == row.part_match.id %} selected='selected' {% endif %}>
{{ part }}
</option>
{% endfor %}
</select>
{% if row.errors.part %}
<p class='help-inline'>{{ row.errors.part }}</p>
{% endif %}
</td>
{% for item in row.data %}
<td>
{% if item.column.guess == 'Part' %}
<i>{{ item.cell }}</i>
{% if row.errors.part %}
<p class='help-inline'>{{ row.errors.part }}</p>
{% endif %}
{% elif item.column.guess == 'Quantity' %}
<input name='quantity_{{ row.index }}' class='numberinput' type='number' min='1' step='any' value='{% decimal row.quantity %}'/>
{% if row.errors.quantity %}
<p class='help-inline'>{{ row.errors.quantity }}</p>
{% endif %}
{% elif item.column.guess == 'Reference' %}
<input name='reference_{{ row.index }}' value='{{ row.reference }}'/>
{% elif item.column.guess == 'Note' %}
<input name='notes_{{ row.index }}' value='{{ row.notes }}'/>
{% elif item.column.guess == 'Overage' %}
<input name='overage_{{ row.index }}' value='{{ row.overage }}'/>
{% else %}
{{ item.cell }}
{% endif %}
<input type='hidden' name='row_{{ row.index }}_col_{{ forloop.counter0 }}' value='{{ item.cell }}'/>
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</form>
{% endblock %}
{% block js_ready %}
{{ block.super }}
$('.bomselect').select2({
dropdownAutoWidth: true,
matcher: partialMatcher,
});
{% endblock %}

View File

@ -1,5 +1,4 @@
{% extends "order/order_base.html" %} {% extends "order/order_base.html" %}
{% load inventree_extras %} {% load inventree_extras %}
{% load i18n %} {% load i18n %}
{% load static %} {% load static %}
@ -17,7 +16,11 @@
<p>{% trans "Step" %} {{ wizard.steps.step1 }} {% trans "of" %} {{ wizard.steps.count }} <p>{% trans "Step" %} {{ wizard.steps.step1 }} {% trans "of" %} {{ wizard.steps.count }}
{% if description %}- {{ description }}{% endif %}</p> {% if description %}- {{ description }}{% endif %}</p>
<form action="" method="post" enctype="multipart/form-data">{% csrf_token %}
{% block form %}
<form action="" method="post" enctype="multipart/form-data">
{% csrf_token %}
<table> <table>
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{{ wizard.management_form }} {{ wizard.management_form }}
@ -29,15 +32,16 @@
{% else %} {% else %}
{% crispy wizard.form %} {% crispy wizard.form %}
{% endif %} {% endif %}
</table>
{% if wizard.steps.prev %}
{% if wizard.steps.prev %}
<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}" class="save btn btn-default">{% trans "Previous Step" %}</button> <button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}" class="save btn btn-default">{% trans "Previous Step" %}</button>
{% endif %} {% endif %}
<button type="submit" class="save btn btn-default">{% trans "Submit" %}</button> <button type="submit" class="save btn btn-default">{% trans "Upload File" %}</button>
</form> </form>
</table>
{% endblock %} {% endblock form %}
{% endblock details %}
{% block js_ready %} {% block js_ready %}
{{ block.super }} {{ block.super }}

View File

@ -567,21 +567,30 @@ class SalesOrderShip(AjaxUpdateView):
class PurchaseOrderUpload(MultiStepFormView): 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 = [ form_list = [
order_forms.UploadFile, order_forms.UploadFile,
order_forms.MatchField, order_forms.MatchField,
order_forms.MatchPart, 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 = [ form_steps_description = [
_("Upload File"), _("Upload File"),
_("Select Fields"), _("Match Fields"),
_("Select Parts"), _("Match Parts"),
] ]
template_name = "order/po_upload.html"
media_folder = 'order_uploads/' media_folder = 'order_uploads/'
# Used for data table
headers = None
rows = None
columns = None
def get_context_data(self, form, **kwargs): def get_context_data(self, form, **kwargs):
context = super().get_context_data(form=form, **kwargs) context = super().get_context_data(form=form, **kwargs)
@ -589,25 +598,52 @@ class PurchaseOrderUpload(MultiStepFormView):
context.update({'order': order}) 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 return context
def get_form_step_data(self, form): def process_step(self, form):
# print(f'{self.steps.current=}\n{form.data=}') print(f'{self.steps.current=} | {form.data}')
return 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): def get_form_step_files(self, form):
# Check if user completed file upload # Check if user completed file upload
if self.steps.current == '0': if self.steps.current == '0':
# Extract columns and rows from FileManager # Retrieve FileManager instance from form
self.extractDataFromFile(form.file_manager) 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 return form.files
def extractDataFromFile(self, file_manager): def post(self, request, *args, **kwargs):
""" Read data from the file """ """ Perform the various 'POST' requests required.
"""
self.columns = file_manager.columns() print('Posting!')
self.rows = file_manager.rows() return super().post(*args, **kwargs)
def done(self, form_list, **kwargs): def done(self, form_list, **kwargs):
return HttpResponseRedirect(reverse('po-detail', kwargs={'pk': self.kwargs['pk']})) return HttpResponseRedirect(reverse('po-detail', kwargs={'pk': self.kwargs['pk']}))