mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Added match parts step, need to process fields data
This commit is contained in:
parent
7cdf0af04a
commit
64fb492b97
@ -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 """
|
||||
|
||||
@ -70,6 +65,37 @@ class FileManager:
|
||||
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
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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 %}
|
112
InvenTree/order/templates/order/order_wizard/match_parts.html
Normal file
112
InvenTree/order/templates/order/order_wizard/match_parts.html
Normal 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 %}
|
@ -1,5 +1,4 @@
|
||||
{% extends "order/order_base.html" %}
|
||||
|
||||
{% load inventree_extras %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
@ -17,7 +16,11 @@
|
||||
|
||||
<p>{% trans "Step" %} {{ wizard.steps.step1 }} {% trans "of" %} {{ wizard.steps.count }}
|
||||
{% 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>
|
||||
{% load crispy_forms_tags %}
|
||||
{{ wizard.management_form }}
|
||||
@ -29,15 +32,16 @@
|
||||
{% else %}
|
||||
{% crispy wizard.form %}
|
||||
{% 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>
|
||||
{% 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>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
{% endblock form %}
|
||||
{% endblock details %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
@ -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']}))
|
||||
|
Loading…
Reference in New Issue
Block a user