Match field step is now managed through form

This commit is contained in:
eeintech 2021-05-06 16:05:58 -04:00
parent f79382d96f
commit e31452a6ad
7 changed files with 385 additions and 549 deletions

View File

@ -56,7 +56,7 @@ class FileManager:
raw_data = file.read().decode('utf-8') raw_data = file.read().decode('utf-8')
# Reset stream position to beginning of file # Reset stream position to beginning of file
file.seek(0) file.seek(0)
elif ext in ['.xls', '.xlsx']: elif ext in ['.xls', '.xlsx', '.json', '.yaml', ]:
raw_data = file.read() raw_data = file.read()
# Reset stream position to beginning of file # Reset stream position to beginning of file
file.seek(0) file.seek(0)

View File

@ -5,8 +5,12 @@ Django forms for interacting with common objects
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django import forms
from django.utils.translation import gettext as _
from InvenTree.forms import HelperForm from InvenTree.forms import HelperForm
from .files import FileManager
from .models import InvenTreeSetting from .models import InvenTreeSetting
@ -21,3 +25,77 @@ class SettingEditForm(HelperForm):
fields = [ fields = [
'value' 'value'
] ]
class UploadFile(forms.Form):
""" Step 1 of FileManagementFormView """
file = forms.FileField(
label=_('File'),
help_text=_('Select file to upload'),
)
def __init__(self, *args, **kwargs):
""" Update label and help_text """
# Get file name
name = None
if 'name' in kwargs:
name = kwargs.pop('name')
super().__init__(*args, **kwargs)
if name:
# Update label and help_text with file name
self.fields['file'].label = _(f'{name.title()} File')
self.fields['file'].help_text = _(f'Select {name} file to upload')
def clean_file(self):
"""
Run tabular file validation.
If anything is wrong with the file, it will raise ValidationError
"""
file = self.cleaned_data['file']
# Validate file using FileManager class - will perform initial data validation
# (and raise a ValidationError if there is something wrong with the file)
FileManager.validate(file)
return file
class MatchField(forms.Form):
""" Step 2 of FileManagementFormView """
def __init__(self, *args, **kwargs):
# Get FileManager
file_manager = None
if 'file_manager' in kwargs:
file_manager = kwargs.pop('file_manager')
super().__init__(*args, **kwargs)
# Setup headers
file_manager.setup()
columns = file_manager.columns()
# Find headers choices
headers_choices = [(header, header) for header in file_manager.HEADERS]
# Create column fields
for col in columns:
field_name = col['name']
self.fields[field_name] = forms.ChoiceField(
choices=[('', '-' * 10)] + headers_choices,
required=False,
)
if col['guess']:
self.fields[field_name].initial = col['guess']
class MatchItem(forms.Form):
""" Step 3 of FileManagementFormView """
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

View File

@ -19,6 +19,7 @@ from InvenTree.helpers import str2bool
from . import models from . import models
from . import forms from . import forms
from .files import FileManager
class SettingEdit(AjaxUpdateView): class SettingEdit(AjaxUpdateView):
@ -164,3 +165,283 @@ class MultiStepFormView(SessionWizardView):
context.update({'description': description}) context.update({'description': description})
return context return context
class FileManagementFormView(MultiStepFormView):
""" Setup form wizard to perform the following steps:
1. Upload tabular data file
2. Match headers to InvenTree fields
3. Edit row data and match InvenTree items
"""
name = None
form_list = [
('upload', forms.UploadFile),
('fields', forms.MatchField),
('items', forms.MatchItem),
]
form_steps_description = [
_("Upload File"),
_("Match Fields"),
_("Match Items"),
]
media_folder = 'file_upload/'
extra_context_data = {}
def get_context_data(self, form, **kwargs):
context = super().get_context_data(form=form, **kwargs)
if self.steps.current == 'fields':
# Get columns and row data
columns = self.file_manager.columns()
rows = self.file_manager.rows()
# Optimize for template
for row in rows:
row_data = row['data']
data = []
for idx, item in enumerate(row_data):
data.append({
'cell': item,
'idx': idx,
'column': columns[idx]
})
row['data'] = data
context.update({'rows': rows})
# Load extra context data
print(f'{self.extra_context_data=}')
for key, items in self.extra_context_data.items():
context.update({key: items})
return context
def getFileManager(self, step=None, form=None):
""" Get FileManager instance from uploaded file """
if self.file_manager:
return
if step is not None:
# Retrieve stored files from upload step
upload_files = self.storage.get_step_files('upload')
if upload_files:
# Get file
file = upload_files.get('upload-file', None)
if file:
self.file_manager = FileManager(file=file, name=self.name)
def get_form_kwargs(self, step=None):
""" Update kwargs to dynamically build forms """
print(f'[STEP] {step}')
# Always retrieve FileManager instance from uploaded file
self.getFileManager(step)
if step == 'upload':
if self.name:
# Dynamically build upload form
kwargs = {
'name': self.name
}
return kwargs
elif step == 'fields':
if self.file_manager:
# Dynamically build match field form
kwargs = {
'file_manager': self.file_manager
}
return kwargs
return super().get_form_kwargs()
def getFormTableData(self, form_data):
""" Extract table cell data from form data.
These data are used to maintain state between sessions.
Table data keys are as follows:
col_name_<idx> - Column name at idx as provided in the uploaded file
col_guess_<idx> - Column guess at idx as selected
row_<x>_col<y> - Cell data as provided in the uploaded file
"""
# Store extra context data
self.extra_context_data = {}
# Map the columns
self.column_names = {}
self.column_selections = {}
self.row_data = {}
for item in form_data:
# print(f'{item} | {form_data[item]}')
value = form_data[item]
# Column names as passed as col_name_<idx> where idx is an integer
# Extract the column names
if item.startswith('col_name_'):
try:
col_id = int(item.replace('col_name_', ''))
except ValueError:
continue
self.column_names[value] = col_id
# Extract the column selections (in the 'select fields' view)
if item.startswith('fields-'):
try:
col_name = item.replace('fields-', '')
except ValueError:
continue
self.column_selections[col_name] = value
# Extract the row data
if item.startswith('row_'):
# Item should be of the format row_<r>_col_<c>
s = item.split('_')
if len(s) < 4:
continue
# Ignore row/col IDs which are not correct numeric values
try:
row_id = int(s[1])
col_id = int(s[3])
except ValueError:
continue
if row_id not in self.row_data:
self.row_data[row_id] = {}
self.row_data[row_id][col_id] = value
# self.col_ids = sorted(self.column_names.keys())
# Re-construct the data table
self.rows = []
for row_idx in sorted(self.row_data.keys()):
row = self.row_data[row_idx]
items = []
for col_idx in sorted(row.keys()):
value = row[col_idx]
items.append(value)
self.rows.append({
'index': row_idx,
'data': items,
'errors': {},
})
# Construct the column data
self.columns = []
# Track any duplicate column selections
duplicates = []
for col in self.column_names:
if col in self.column_selections:
guess = self.column_selections[col]
else:
guess = None
header = ({
'name': self.column_names[col],
'guess': guess
})
if guess:
n = list(self.column_selections.values()).count(self.column_selections[col])
if n > 1:
header['duplicate'] = True
duplicates.append(col)
self.columns.append(header)
# Are there any missing columns?
missing_columns = []
# Check that all required fields are present
for col in self.file_manager.REQUIRED_HEADERS:
if col not in self.column_selections.values():
missing_columns.append(col)
# Check that at least one of the part match field is present
part_match_found = False
for col in self.file_manager.PART_MATCH_HEADERS:
if col in self.column_selections.values():
part_match_found = True
break
# If not, notify user
if not part_match_found:
for col in self.file_manager.PART_MATCH_HEADERS:
missing_columns.append(col)
# Store extra context data
self.extra_context_data['missing_columns'] = missing_columns
self.extra_context_data['duplicates'] = duplicates
def checkFieldSelection(self, form):
""" Check field matching """
# Extract form data
self.getFormTableData(form.data)
valid = len(self.extra_context_data.get('missing_columns', [])) == 0 and not self.extra_context_data.get('duplicates', [])
return valid
def validate(self, step, form):
""" Validate forms """
valid = False
# Process steps
if step == 'upload':
# Validation is done during POST
valid = True
elif step == 'fields':
# Validate user form data
valid = self.checkFieldSelection(form)
if not valid:
form.add_error(None, 'Fields matching failed')
elif step == 'items':
# valid = self.checkPartSelection(form)
# if not valid:
# form.add_error(None, 'Items matching failed')
pass
return valid
def post(self, request, *args, **kwargs):
""" Perform validations before posting data """
wizard_goto_step = self.request.POST.get('wizard_goto_step', None)
form = self.get_form(data=self.request.POST, files=self.request.FILES)
form_valid = self.validate(self.steps.current, form)
if not form_valid and not wizard_goto_step:
# Re-render same step
return self.render(form)
print('\nPosting... ')
return super().post(*args, **kwargs)

View File

@ -14,8 +14,6 @@ from InvenTree.forms import HelperForm
from InvenTree.fields import RoundingDecimalFormField from InvenTree.fields import RoundingDecimalFormField
from InvenTree.fields import DatePickerFormField from InvenTree.fields import DatePickerFormField
from common.files import FileManager
import part.models import part.models
from stock.models import StockLocation from stock.models import StockLocation
@ -286,30 +284,3 @@ class EditSalesOrderAllocationForm(HelperForm):
'line', 'line',
'item', 'item',
'quantity'] 'quantity']
class UploadFile(forms.Form):
""" Step 1 """
file = forms.FileField(
label=_('Order File'),
help_text=_('Select order file to upload'),
)
def clean_file(self):
file = self.cleaned_data['file']
# Validate file using FileManager class - will perform initial data validation
# (and raise a ValidationError if there is something wrong with the file)
FileManager.validate(file)
return file
class MatchField(forms.Form):
""" Step 2 """
pass
class MatchPart(forms.Form):
""" Step 3 """
pass

View File

@ -15,12 +15,17 @@
</ul> </ul>
</div> </div>
{% endif %} {% endif %}
{% if duplicates and duplicates|length > 0 %}
<div class='alert alert-danger alert-block' role='alert'>
{% trans "Duplicate selections found, see below. Fix them then retry submitting." %}
</div>
{% endif %}
{% endblock form_alert %} {% endblock form_alert %}
{% block form_buttons_top %} {% block form_buttons_top %}
{% comment %} {% 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 %} {% endcomment %} {% endif %}
<button type="submit" class="save btn btn-default">{% trans "Submit Selections" %}</button> <button type="submit" class="save btn btn-default">{% trans "Submit Selections" %}</button>
{% endblock form_buttons_top %} {% endblock form_buttons_top %}
@ -29,7 +34,7 @@
<tr> <tr>
<th></th> <th></th>
<th>{% trans "File Fields" %}</th> <th>{% trans "File Fields" %}</th>
{% for col in columns %} {% for col in form %}
<th> <th>
<div> <div>
<input type='hidden' name='col_name_{{ forloop.counter0 }}' value='{{ col.name }}'/> <input type='hidden' name='col_name_{{ forloop.counter0 }}' value='{{ col.name }}'/>
@ -46,17 +51,22 @@
<tr> <tr>
<td></td> <td></td>
<td>{% trans "Match Fields" %}</td> <td>{% trans "Match Fields" %}</td>
{% for col in columns %} {% for col in form %}
<td> <td>
<select class='select' id='id_col_{{ forloop.counter0 }}' name='col_guess_{{ forloop.counter0 }}'> {{ col }}
{% comment %} <select class='select' id='id_col_{{ forloop.counter0 }}' name='col_guess_{{ forloop.counter0 }}'>
<option value=''>---------</option> <option value=''>---------</option>
{% for req in headers %} {% for req in headers %}
<option value='{{ req }}'{% if req == col.guess %}selected='selected'{% endif %}>{{ req }}</option> <option value='{{ req }}'{% if req == col.guess %}selected='selected'{% endif %}>{{ req }}</option>
{% endfor %} {% endfor %}
</select> </select> {% endcomment %}
{% if col.duplicate %} {% for duplicate in duplicates %}
<p class='help-inline'>{% trans "Duplicate column selection" %}</p> {% if duplicate == col.name %}
{% endif %} <div class='alert alert-danger alert-block text-center' role='alert' style='padding:2px; margin-top:6px; margin-bottom:2px'>
<b>{% trans "Duplicate selection" %}</b>
</div>
{% endif %}
{% endfor %}
</td> </td>
{% endfor %} {% endfor %}
</tr> </tr>

View File

@ -12,9 +12,9 @@
{% endblock form_alert %} {% endblock form_alert %}
{% block form_buttons_top %} {% block form_buttons_top %}
{% comment %} {% 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 %} {% endcomment %} {% endif %}
<button type="submit" class="save btn btn-default">{% trans "Submit Selections" %}</button> <button type="submit" class="save btn btn-default">{% trans "Submit Selections" %}</button>
{% endblock form_buttons_top %} {% endblock form_buttons_top %}

View File

@ -28,8 +28,7 @@ from stock.models import StockItem, StockLocation
from part.models import Part from part.models import Part
from common.models import InvenTreeSetting from common.models import InvenTreeSetting
from common.views import MultiStepFormView from common.views import FileManagementFormView
from common.files import FileManager
from . import forms as order_forms from . import forms as order_forms
@ -567,14 +566,10 @@ class SalesOrderShip(AjaxUpdateView):
return self.renderJsonResponse(request, form, data, context) return self.renderJsonResponse(request, form, data, context)
class PurchaseOrderUpload(MultiStepFormView): class PurchaseOrderUpload(FileManagementFormView):
''' PurchaseOrder: Upload file, match to fields and parts (using multi-Step form) ''' ''' PurchaseOrder: Upload file, match to fields and parts (using multi-Step form) '''
form_list = [ name = 'order'
('upload', order_forms.UploadFile),
('fields', order_forms.MatchField),
('parts', order_forms.MatchPart),
]
form_steps_template = [ form_steps_template = [
'order/order_wizard/po_upload.html', 'order/order_wizard/po_upload.html',
'order/order_wizard/match_fields.html', 'order/order_wizard/match_fields.html',
@ -583,16 +578,8 @@ class PurchaseOrderUpload(MultiStepFormView):
form_steps_description = [ form_steps_description = [
_("Upload File"), _("Upload File"),
_("Match Fields"), _("Match Fields"),
_("Match Parts"), _("Match Supplier Parts"),
] ]
media_folder = 'order_uploads/'
# Used for data table
headers = None
rows = None
columns = None
missing_columns = None
allowed_parts = 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)
@ -601,499 +588,8 @@ 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:
for row in self.rows:
row_data = row['data']
data = []
for idx, item in enumerate(row_data):
data.append({
'cell': item,
'idx': idx,
'column': self.columns[idx]
})
row['data'] = data
context.update({'rows': self.rows})
# print(f'{self.rows}')
if self.missing_columns:
context.update({'missing_columns': self.missing_columns})
return context return context
def getTableDataFromForm(self, form_data):
""" Extract table cell data from form data.
These data are used to maintain state between sessions.
Table data keys are as follows:
col_name_<idx> - Column name at idx as provided in the uploaded file
col_guess_<idx> - Column guess at idx as selected
row_<x>_col<y> - Cell data as provided in the uploaded file
"""
# Map the columns
self.column_names = {}
self.column_selections = {}
self.row_data = {}
for item in form_data:
value = form_data[item]
# Column names as passed as col_name_<idx> where idx is an integer
# Extract the column names
if item.startswith('col_name_'):
try:
col_id = int(item.replace('col_name_', ''))
except ValueError:
continue
col_name = value
self.column_names[col_id] = col_name
# Extract the column selections (in the 'select fields' view)
if item.startswith('col_guess_'):
try:
col_id = int(item.replace('col_guess_', ''))
except ValueError:
continue
col_name = value
self.column_selections[col_id] = value
# Extract the row data
if item.startswith('row_'):
# Item should be of the format row_<r>_col_<c>
s = item.split('_')
if len(s) < 4:
continue
# Ignore row/col IDs which are not correct numeric values
try:
row_id = int(s[1])
col_id = int(s[3])
except ValueError:
continue
if row_id not in self.row_data:
self.row_data[row_id] = {}
self.row_data[row_id][col_id] = value
self.col_ids = sorted(self.column_names.keys())
# Re-construct the data table
self.rows = []
for row_idx in sorted(self.row_data.keys()):
row = self.row_data[row_idx]
items = []
for col_idx in sorted(row.keys()):
value = row[col_idx]
items.append(value)
self.rows.append({
'index': row_idx,
'data': items,
'errors': {},
})
# Construct the column data
self.columns = []
# Track any duplicate column selections
self.duplicates = False
for col in self.col_ids:
if col in self.column_selections:
guess = self.column_selections[col]
else:
guess = None
header = ({
'name': self.column_names[col],
'guess': guess
})
if guess:
n = list(self.column_selections.values()).count(self.column_selections[col])
if n > 1:
header['duplicate'] = True
self.duplicates = True
self.columns.append(header)
# Are there any missing columns?
self.missing_columns = []
# Check that all required fields are present
for col in self.file_manager.REQUIRED_HEADERS:
if col not in self.column_selections.values():
self.missing_columns.append(col)
# Check that at least one of the part match field is present
part_match_found = False
for col in self.file_manager.PART_MATCH_HEADERS:
if col in self.column_selections.values():
part_match_found = True
break
# If not, notify user
if not part_match_found:
for col in self.file_manager.PART_MATCH_HEADERS:
self.missing_columns.append(col)
def getColumnIndex(self, name):
""" Return the index of the column with the given name.
It named column is not found, return -1
"""
try:
idx = list(self.column_selections.values()).index(name)
except ValueError:
idx = -1
return idx
def preFillSelections(self):
""" Once data columns have been selected, attempt to pre-select the proper data from the database.
This function is called once the field selection has been validated.
The pre-fill data are then passed through to the part selection form.
"""
# Fields prefixed with "Part_" can be used to do "smart matching" against Part objects in the database
q_idx = self.getColumnIndex('Quantity')
s_idx = self.getColumnIndex('Supplier_SKU')
# m_idx = self.getColumnIndex('Manufacturer_MPN')
# p_idx = self.getColumnIndex('Unit_Price')
# e_idx = self.getColumnIndex('Extended_Price')
for row in self.rows:
# Initially use a quantity of zero
quantity = Decimal(0)
# Initially we do not have a part to reference
exact_match_part = None
# A list of potential Part matches
part_options = self.allowed_parts
# Check if there is a column corresponding to "quantity"
if q_idx >= 0:
q_val = row['data'][q_idx]
if q_val:
# Delete commas
q_val = q_val.replace(',','')
try:
# Attempt to extract a valid quantity from the field
quantity = Decimal(q_val)
except (ValueError, InvalidOperation):
pass
# Store the 'quantity' value
row['quantity'] = quantity
# Check if there is a column corresponding to "Supplier SKU"
if s_idx >= 0:
sku = row['data'][s_idx]
try:
# Attempt SupplierPart lookup based on SKU value
exact_match_part = SupplierPart.objects.get(SKU__contains=sku)
except (ValueError, SupplierPart.DoesNotExist, SupplierPart.MultipleObjectsReturned):
exact_match_part = None
# Check if there is a column corresponding to "Manufacturer MPN"
# if m_idx >= 0:
# row['part_mpn'] = row['data'][m_idx]
# try:
# # Attempt ManufacturerPart lookup based on MPN value
# exact_match_part = ManufacturerPart.objects.get(MPN=row['part_mpn'])
# except (ValueError, ManufacturerPart.DoesNotExist):
# exact_match_part = None
# Supply list of part options for each row, sorted by how closely they match the part name
row['part_options'] = part_options
# Unless found, the 'part_match' is blank
row['part_match'] = None
if exact_match_part:
# If there is an exact match based on SKU or MPN, use that
row['part_match'] = exact_match_part
def updatePartSelectionColumns(self, form):
# for idx, row in enumerate(self.rows):
# print(f'{idx} | {row}\n\n')
pass
def getFileManager(self, form=None):
""" Create FileManager instance from upload file """
if self.file_manager:
return
if self.steps.current == 'upload':
# Get file from form data
order_file = form.cleaned_data['file']
self.file_manager = FileManager(file=order_file, name='order')
else:
# Retrieve stored files from upload step
upload_files = self.storage.get_step_files('upload')
# Get file
order_file = upload_files.get('upload-file', None)
if order_file:
self.file_manager = FileManager(file=order_file, name='order')
def setupFieldSelection(self, form):
""" Setup fields form """
# Get FileManager
self.getFileManager(form)
# Setup headers
self.file_manager.setup()
# Set headers
self.headers = self.file_manager.HEADERS
# Set columns and rows
self.columns = self.file_manager.columns()
self.rows = self.file_manager.rows()
def handleFieldSelection(self, form):
""" Process field matching """
# Retrieve FileManager instance from uploaded file
self.getFileManager(form)
# Update headers
if self.file_manager:
self.file_manager.setup()
else:
return False
# Extract form data
self.getTableDataFromForm(form.data)
valid = len(self.missing_columns) == 0 and not self.duplicates
return valid
def getRowByIndex(self, idx):
for row in self.rows:
if row['index'] == idx:
return row
return None
def handlePartSelection(self, form):
# Retrieve FileManager instance from uploaded file
self.getFileManager(form)
# Extract form data
self.getTableDataFromForm(form.data)
# Keep track of the parts that have been selected
parts = {}
# Extract other data (part selections, etc)
for key, value in form.data.items():
# Extract quantity from each row
if key.startswith('quantity_'):
try:
row_id = int(key.replace('quantity_', ''))
row = self.getRowByIndex(row_id)
if row is None:
continue
q = Decimal(1)
try:
q = Decimal(value)
if q < 0:
row['errors']['quantity'] = _('Quantity must be greater than zero')
if 'part' in row.keys():
if row['part'].trackable:
# Trackable parts must use integer quantities
if not q == int(q):
row['errors']['quantity'] = _('Quantity must be integer value for trackable parts')
except (ValueError, InvalidOperation):
row['errors']['quantity'] = _('Enter a valid quantity')
row['quantity'] = q
except ValueError:
continue
# Extract part from each row
if key.startswith('part_'):
try:
row_id = int(key.replace('part_', ''))
row = self.getRowByIndex(row_id)
if row is None:
continue
except ValueError:
# Row ID non integer value
continue
try:
part_id = int(value)
part = Part.objects.get(id=part_id)
except ValueError:
row['errors']['part'] = _('Select valid part')
continue
except Part.DoesNotExist:
row['errors']['part'] = _('Select valid part')
continue
# Keep track of how many of each part we have seen
if part_id in parts:
parts[part_id]['quantity'] += 1
row['errors']['part'] = _('Duplicate part selected')
else:
parts[part_id] = {
'part': part,
'quantity': 1,
}
row['part'] = part
if part.trackable:
# For trackable parts, ensure the quantity is an integer value!
if 'quantity' in row.keys():
q = row['quantity']
if not q == int(q):
row['errors']['quantity'] = _('Quantity must be integer value for trackable parts')
# Extract other fields which do not require further validation
for field in ['reference', 'notes']:
if key.startswith(field + '_'):
try:
row_id = int(key.replace(field + '_', ''))
row = self.getRowByIndex(row_id)
if row:
row[field] = value
except:
continue
# Are there any errors after form handling?
valid = True
for row in self.rows:
# Has a part been selected for the given row?
part = row.get('part', None)
if part is None:
row['errors']['part'] = _('Select a part')
# Has a quantity been specified?
if row.get('quantity', None) is None:
row['errors']['quantity'] = _('Specify quantity')
errors = row.get('errors', [])
if len(errors) > 0:
valid = False
return valid
def get_form_step_data(self, form):
""" Process form data after it has been posted """
# print(f'{self.steps.current=}\n{form.data=}')
# Retrieve FileManager instance from uploaded file
self.getFileManager(form)
# print(f'{self.file_manager=}')
# Process steps
if self.steps.current == 'upload':
self.setupFieldSelection(form)
elif self.steps.current == 'fields':
self.allowed_parts = SupplierPart.objects.all()
self.rows = self.file_manager.rows()
self.preFillSelections()
# self.updatePartSelectionColumns(form)
# elif self.steps.current == 'parts':
# self.handlePartSelection(form)
return form.data
def validate(self, step, form):
""" Validate forms """
valid = False
# Process steps
if step == 'upload':
# Validation is done during POST
valid = True
elif step == 'fields':
# Validate user form data
valid = self.handleFieldSelection(form)
if not valid:
form.add_error(None, 'Fields matching failed')
# Reload headers
self.headers = self.file_manager.HEADERS
elif step == 'parts':
valid = self.handlePartSelection(form)
# if not valid:
# pass
return valid
def post(self, request, *args, **kwargs):
""" Perform validations before posting data """
wizard_goto_step = self.request.POST.get('wizard_goto_step', None)
form = self.get_form(data=self.request.POST, files=self.request.FILES)
print(f'\nCurrent step = {self.steps.current}')
form_valid = self.validate(self.steps.current, form)
if not form_valid and not wizard_goto_step:
# Re-render same step
return self.render(form)
print('\nPosting... ')
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']}))