From 4f942fd9f7506fde7fcaeff5aa307e9192f57a69 Mon Sep 17 00:00:00 2001 From: eeintech Date: Thu, 6 May 2021 17:07:22 -0400 Subject: [PATCH] Working towards item match form --- InvenTree/common/forms.py | 39 +++++- InvenTree/common/views.py | 115 ++++++++++++++++-- .../order/order_wizard/match_parts.html | 14 ++- 3 files changed, 152 insertions(+), 16 deletions(-) diff --git a/InvenTree/common/forms.py b/InvenTree/common/forms.py index ba349808e1..6238320c75 100644 --- a/InvenTree/common/forms.py +++ b/InvenTree/common/forms.py @@ -77,10 +77,11 @@ class MatchField(forms.Form): super().__init__(*args, **kwargs) - # Setup headers + # Setup FileManager file_manager.setup() + # Get columns columns = file_manager.columns() - # Find headers choices + # Get headers choices headers_choices = [(header, header) for header in file_manager.HEADERS] # Create column fields @@ -98,4 +99,38 @@ class MatchItem(forms.Form): """ Step 3 of FileManagementFormView """ def __init__(self, *args, **kwargs): + + # Get FileManager + file_manager = None + if 'file_manager' in kwargs: + file_manager = kwargs.pop('file_manager') + + if 'row_data' in kwargs: + row_data = kwargs.pop('row_data') + super().__init__(*args, **kwargs) + + # Setup FileManager + file_manager.setup() + # Get columns + columns = file_manager.columns() + + # Create fields + # 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, + required=True, + ) + if item_match: + self.fields[field_name].initial = item_match diff --git a/InvenTree/common/views.py b/InvenTree/common/views.py index f8993584ab..98fad524c4 100644 --- a/InvenTree/common/views.py +++ b/InvenTree/common/views.py @@ -6,6 +6,8 @@ Django views for interacting with common models from __future__ import unicode_literals import os +import ast +from decimal import Decimal, InvalidOperation from django.utils.translation import ugettext_lazy as _ from django.forms import CheckboxInput, Select @@ -21,6 +23,8 @@ from . import models from . import forms from .files import FileManager +from part.models import SupplierPart + class SettingEdit(AjaxUpdateView): """ @@ -243,19 +247,25 @@ class FileManagementFormView(MultiStepFormView): self.getFileManager(step) if step == 'upload': + # Dynamically build upload form 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 + # Dynamically build match field form + kwargs = { + 'file_manager': self.file_manager + } + return kwargs + elif step == 'items': + # Dynamically build match item form + kwargs = {} + kwargs['file_manager'] = self.file_manager + self.getFieldSelections() + kwargs['row_data'] = self.rows + return kwargs return super().get_form_kwargs() @@ -281,7 +291,7 @@ class FileManagementFormView(MultiStepFormView): self.row_data = {} for item in form_data: - # print(f'{item} | {form_data[item]}') + # print(f'{item} | {form_data[item]} | {type(form_data[item])}') value = form_data[item] # Column names as passed as col_name_ where idx is an integer @@ -323,7 +333,9 @@ class FileManagementFormView(MultiStepFormView): if row_id not in self.row_data: self.row_data[row_id] = {} - self.row_data[row_id][col_id] = value + # TODO: this is a hack + value = value.replace("'", '"') + self.row_data[row_id][col_id] = ast.literal_eval(value) # self.col_ids = sorted(self.column_names.keys()) @@ -395,6 +407,91 @@ class FileManagementFormView(MultiStepFormView): self.extra_context_data['missing_columns'] = missing_columns self.extra_context_data['duplicates'] = duplicates + 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 getFieldSelections(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') + + self.allowed_items = SupplierPart.objects.all() + + 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 + 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'] + + 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['item_options'] = item_options + + # Unless found, the 'part_match' is blank + row['item_match'] = None + + if exact_match_part: + # If there is an exact match based on SKU or MPN, use that + row['item_match'] = exact_match_part + def checkFieldSelection(self, form): """ Check field matching """ diff --git a/InvenTree/order/templates/order/order_wizard/match_parts.html b/InvenTree/order/templates/order/order_wizard/match_parts.html index 63cac689b4..abfe66577b 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 rows %} + {% for row in form %} - {% add row.index 1 %} - {% if row.errors.part %}

{{ row.errors.part }}

- {% endif %} + {% endif %} {% endcomment %} {% for item in row.data %} + {% comment %} {% if item.column.guess == 'Quantity' %} {% if row.errors.quantity %} @@ -74,7 +78,7 @@ {% else %} {{ item.cell }} {% endif %} - + {% endcomment %} {% endfor %}