diff --git a/InvenTree/common/files.py b/InvenTree/common/files.py index 6de08731a6..91fdc61a10 100644 --- a/InvenTree/common/files.py +++ b/InvenTree/common/files.py @@ -20,8 +20,8 @@ class FileManager: # Fields which are absolutely necessary for valid upload REQUIRED_HEADERS = [] - # Fields which are used for part matching (only one of them is needed) - PART_MATCH_HEADERS = [] + # Fields which are used for item matching (only one of them is needed) + ITEM_MATCH_HEADERS = [] # Fields which would be helpful but are not required OPTIONAL_HEADERS = [] @@ -83,7 +83,7 @@ class FileManager: def update_headers(self): """ Update headers """ - self.HEADERS = self.REQUIRED_HEADERS + self.PART_MATCH_HEADERS + self.OPTIONAL_HEADERS + self.HEADERS = self.REQUIRED_HEADERS + self.ITEM_MATCH_HEADERS + self.OPTIONAL_HEADERS def setup(self): """ Setup headers depending on the file name """ @@ -96,7 +96,7 @@ class FileManager: 'Quantity', ] - self.PART_MATCH_HEADERS = [ + self.ITEM_MATCH_HEADERS = [ 'Manufacturer_MPN', 'Supplier_SKU', ] diff --git a/InvenTree/common/forms.py b/InvenTree/common/forms.py index 6238320c75..8b09c28929 100644 --- a/InvenTree/common/forms.py +++ b/InvenTree/common/forms.py @@ -119,18 +119,40 @@ class MatchItem(forms.Form): # Item selection for row in row_data: for col in row['data']: - print(f'{row["index"]=} | {col["column"]["guess"]=}') - if col['column']['guess']: - if col['column']['guess'] in file_manager.PART_MATCH_HEADERS: - # Get item options - item_options = row['item_options'] - # Get item match - item_match = row['item_match'] - - field_name = col['column']['guess'].lower() + '_' + str(row['index']) - self.fields[field_name] = forms.ChoiceField( - choices=item_options, + if col['column']['guess'] in file_manager.REQUIRED_HEADERS: + field_name = col['column']['guess'].lower() + '-' + str(row['index'] - 1) + if 'quantity' in col['column']['guess'].lower(): + self.fields[field_name] = forms.CharField( required=True, + widget=forms.NumberInput(attrs={ + 'name': 'quantity' + str(row['index']), + 'class': 'numberinput', + 'type': 'number', + 'min': '1', + 'step': 'any', + 'value': row['quantity'], + }) ) - if item_match: - self.fields[field_name].initial = item_match + else: + self.fields[field_name] = forms.Input( + required=True, + widget=forms.Select(attrs={ + }) + ) + elif col['column']['guess'] in file_manager.ITEM_MATCH_HEADERS: + print(f'{row["index"]=} | {col["column"]["guess"]=} | {row.get("item_match", "No Match")}') + + # Get item options + item_options = [(option.id, option) for option in row['item_options']] + # Get item match + item_match = row['item_match'] + + field_name = col['column']['guess'].lower() + '-' + str(row['index'] - 1) + self.fields[field_name] = forms.ChoiceField( + choices=[('', '-' * 10)] + item_options, + required=True, + widget=forms.Select(attrs={'class': 'bomselect'}) + ) + if item_match: + print(f'{item_match=}') + self.fields[field_name].initial = item_match.id diff --git a/InvenTree/common/views.py b/InvenTree/common/views.py index 98fad524c4..fd2b452527 100644 --- a/InvenTree/common/views.py +++ b/InvenTree/common/views.py @@ -23,7 +23,7 @@ from . import models from . import forms from .files import FileManager -from part.models import SupplierPart +from company.models import ManufacturerPart, SupplierPart class SettingEdit(AjaxUpdateView): @@ -195,12 +195,41 @@ class FileManagementFormView(MultiStepFormView): def get_context_data(self, form, **kwargs): context = super().get_context_data(form=form, **kwargs) - if self.steps.current == 'fields': + if self.steps.current == 'fields' or self.steps.current == 'items': # Get columns and row data columns = self.file_manager.columns() rows = self.file_manager.rows() + + + key_item_select = '' + key_quantity_select = '' + if self.steps.current == 'items': + # Get file manager + self.getFileManager() + # Find column key for item selection + for item in self.file_manager.ITEM_MATCH_HEADERS: + item = item.lower() + for key in form.fields.keys(): + print(f'{item=} is in {key=} ?') + if item in key: + key_item_select = item + break + break + + # Find column key for quantity selection + key_quantity_select = 'quantity' + # Optimize for template for row in rows: + + # Add item select field + if key_item_select: + row['item_select'] = key_item_select + '-' + str(row['index']) + print(f'{row["item_select"]}') + # Add quantity select field + if key_quantity_select: + row['quantity_select'] = key_quantity_select + '-' + str(row['index']) + row_data = row['data'] data = [] @@ -209,12 +238,16 @@ class FileManagementFormView(MultiStepFormView): data.append({ 'cell': item, 'idx': idx, - 'column': columns[idx] + 'column': columns[idx], }) row['data'] = data + print(f'\n{row=}') + context.update({'rows': rows}) + if self.steps.current == 'items': + context.update({'columns': columns}) # Load extra context data print(f'{self.extra_context_data=}') @@ -393,14 +426,14 @@ class FileManagementFormView(MultiStepFormView): # 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: + for col in self.file_manager.ITEM_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: + for col in self.file_manager.ITEM_MATCH_HEADERS: missing_columns.append(col) # Store extra context data @@ -425,15 +458,16 @@ class FileManagementFormView(MultiStepFormView): The pre-fill data are then passed through to the part selection form. """ + match_supplier = False + match_manufacturer = False + # 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') + m_idx = self.getColumnIndex('Manufacturer_MPN') # p_idx = self.getColumnIndex('Unit_Price') # e_idx = self.getColumnIndex('Extended_Price') - self.allowed_items = SupplierPart.objects.all() - for row in self.rows: # Initially use a quantity of zero @@ -442,9 +476,6 @@ class FileManagementFormView(MultiStepFormView): # Initially we do not have a part to reference exact_match_part = None - # A list of potential Part matches - item_options = self.allowed_items - # Check if there is a column corresponding to "quantity" if q_idx >= 0: q_val = row['data'][q_idx]['cell'] @@ -464,7 +495,10 @@ class FileManagementFormView(MultiStepFormView): # Check if there is a column corresponding to "Supplier SKU" if s_idx >= 0: - sku = row['data'][s_idx] + sku = row['data'][s_idx]['cell'] + + # Match for supplier + match_supplier = True try: # Attempt SupplierPart lookup based on SKU value @@ -473,17 +507,27 @@ class FileManagementFormView(MultiStepFormView): 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] + if m_idx >= 0: + mpn = row['data'][m_idx]['cell'] + + # Match for manufacturer + if not match_supplier: + match_manufacturer = True + + try: + # Attempt ManufacturerPart lookup based on MPN value + exact_match_part = ManufacturerPart.objects.get(MPN__contains=mpn) + except (ValueError, ManufacturerPart.DoesNotExist, ManufacturerPart.MultipleObjectsReturned): + exact_match_part = None + + # Check if matching for supplier or manufacturer parts + if match_supplier: + self.allowed_items = SupplierPart.objects.all() + elif match_manufacturer: + self.allowed_items = ManufacturerPart.objects.all() - # 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['item_options'] = item_options + row['item_options'] = self.allowed_items # Unless found, the 'part_match' is blank row['item_match'] = None @@ -502,6 +546,16 @@ class FileManagementFormView(MultiStepFormView): return valid + def checkPartSelection(self, form): + """ Check part 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 """ @@ -519,11 +573,10 @@ class FileManagementFormView(MultiStepFormView): form.add_error(None, 'Fields matching failed') elif step == 'items': - # valid = self.checkPartSelection(form) + valid = self.checkPartSelection(form) - # if not valid: - # form.add_error(None, 'Items matching failed') - pass + if not valid: + form.add_error(None, 'Items matching failed') return valid diff --git a/InvenTree/order/templates/order/order_wizard/match_parts.html b/InvenTree/order/templates/order/order_wizard/match_parts.html index abfe66577b..9994c9d41e 100644 --- a/InvenTree/order/templates/order/order_wizard/match_parts.html +++ b/InvenTree/order/templates/order/order_wizard/match_parts.html @@ -39,7 +39,7 @@ - {% for row in form %} + {% for row in rows %} + {% for field in form.visible_fields %} + {% if field.name == row.item_select %} + {{ field }} + {% endif %} + {% endfor %} + {% comment %} + {% for field in form.visible_fields %} + {% if field.name == row.quantity_select %} + {{ field }} + {% endif %} + {% endfor %} + {% comment %} {% endcomment %} {% if row.errors.quantity %}

{{ row.errors.quantity }}

{% endif %} {% else %} {{ item.cell }} {% endif %} - {% endcomment %} + {% endfor %}