From ceccdf39093a078403723dad431c7c78ba854f2e Mon Sep 17 00:00:00 2001 From: eeintech Date: Fri, 7 May 2021 18:00:30 -0400 Subject: [PATCH] Now creating PurchaseOrderLineItem instances! --- InvenTree/common/forms.py | 20 ++- InvenTree/common/views.py | 152 ++---------------- .../order/order_wizard/match_parts.html | 4 +- InvenTree/order/views.py | 134 ++++++++++++++- 4 files changed, 164 insertions(+), 146 deletions(-) diff --git a/InvenTree/common/forms.py b/InvenTree/common/forms.py index 3c1220dd0f..63ed74c378 100644 --- a/InvenTree/common/forms.py +++ b/InvenTree/common/forms.py @@ -114,16 +114,18 @@ class MatchItem(forms.Form): # Setup FileManager file_manager.setup() - # Get columns - columns = file_manager.columns() + # Create fields if row_data: - # Create fields + # Navigate row data for row in row_data: + # Navigate column data for col in row['data']: - # print(f"{col=}") + # Create input for required headers if col['column']['guess'] in file_manager.REQUIRED_HEADERS: + # Set field name field_name = col['column']['guess'].lower() + '-' + str(row['index']) + # Set field input box if 'quantity' in col['column']['guess'].lower(): self.fields[field_name] = forms.CharField( required=True, @@ -142,15 +144,16 @@ class MatchItem(forms.Form): widget=forms.Select(attrs={ }) ) + + # Create item selection box 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']) + # Set field name + field_name = 'item_select-' + str(row['index']) + # Set field select box self.fields[field_name] = forms.ChoiceField( choices=[('', '-' * 10)] + item_options, required=True, @@ -158,5 +161,6 @@ class MatchItem(forms.Form): 'class': 'select bomselect', }) ) + # Update initial selection if item_match: self.fields[field_name].initial = item_match.id diff --git a/InvenTree/common/views.py b/InvenTree/common/views.py index a5ad21b667..1e54663862 100644 --- a/InvenTree/common/views.py +++ b/InvenTree/common/views.py @@ -6,8 +6,6 @@ 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 @@ -23,8 +21,6 @@ from . import models from . import forms from .files import FileManager -from company.models import ManufacturerPart, SupplierPart - class SettingEdit(AjaxUpdateView): """ @@ -192,19 +188,26 @@ class FileManagementFormView(MultiStepFormView): media_folder = 'file_upload/' extra_context_data = {} + # Set keys for item matching + key_item_select = 'item_select' + key_quantity_select = 'quantity' + def get_context_data(self, form, **kwargs): context = super().get_context_data(form=form, **kwargs) if self.steps.current == 'fields' or self.steps.current == 'items': + # Get columns and row data self.columns = self.file_manager.columns() self.rows = self.file_manager.rows() + # Check for stored data + stored_data = self.storage.get_step_data(self.steps.current) + if stored_data: + self.get_form_table_data(stored_data) # Set form table data self.set_form_table_data(form=form) - # if self.steps.current == 'items': - # for row in self.rows: - # print(f'{row=}') + # Update context context.update({'rows': self.rows}) context.update({'columns': self.columns}) @@ -288,8 +291,6 @@ class FileManagementFormView(MultiStepFormView): self.row_data = {} for item, value in form_data.items(): - # 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 @@ -312,32 +313,6 @@ class FileManagementFormView(MultiStepFormView): self.column_selections[col_name] = value - # Extract the row data - if item.startswith('row_'): - # Item should be of the format row__col_ - 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] = {} - - # TODO: this is a hack - value = value.replace("'", '"') - # print(f'{type(value)=} | {value=}') - try: - self.row_data[row_id][col_id] = ast.literal_eval(value) - except (ValueError, SyntaxError): - pass - def set_form_table_data(self, form=None): if self.row_data: # Re-construct the row data @@ -375,29 +350,12 @@ class FileManagementFormView(MultiStepFormView): # In the item selection step: update row data to contain fields if form and self.steps.current == 'items': - key_item_select = '' - key_quantity_select = '' - - # Find column key for item selection - for item in self.file_manager.ITEM_MATCH_HEADERS: - item = item.lower() - for key in form.fields.keys(): - if item in key: - key_item_select = item - break - break - - # Find column key for quantity selection - key_quantity_select = 'quantity' - # Update row data for row in self.rows: # Add item select field - if key_item_select: - row['item_select'] = key_item_select + '-' + str(row['index']) + row['item_select'] = self.key_item_select + '-' + str(row['index']) # Add quantity select field - if key_quantity_select: - row['quantity_select'] = key_quantity_select + '-' + str(row['index']) + row['quantity_select'] = self.key_quantity_select + '-' + str(row['index']) if self.column_names: # Re-construct the column data @@ -406,7 +364,7 @@ class FileManagementFormView(MultiStepFormView): for key in self.column_names: header = ({ 'name': key, - 'guess': self.column_selections[key], + 'guess': self.column_selections.get(key, ''), }) self.columns.append(header) @@ -426,87 +384,11 @@ class FileManagementFormView(MultiStepFormView): """ 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. + + This method is very specific to the type of data found in the file, + therefore overwrite it in the subclass. """ - - match_supplier = False - match_manufacturer = False - self.allowed_items = None - - # Fields prefixed with "Part_" can be used to do "smart matching" against Part objects in the database - q_idx = self.get_column_index('Quantity') - s_idx = self.get_column_index('Supplier_SKU') - m_idx = self.get_column_index('Manufacturer_MPN') - # p_idx = self.get_column_index('Unit_Price') - # e_idx = self.get_column_index('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 - - # 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: - print(f'{row["data"][s_idx]=}') - sku = row['data'][s_idx]['cell'] - - # Match for supplier - match_supplier = True - - 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: - 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() - - # Supply list of part options for each row, sorted by how closely they match the part name - row['item_options'] = self.allowed_items - - # 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 + pass def check_field_selection(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 6608263ffb..e6cb565a5b 100644 --- a/InvenTree/order/templates/order/order_wizard/match_parts.html +++ b/InvenTree/order/templates/order/order_wizard/match_parts.html @@ -24,7 +24,7 @@ {% trans "Row" %} - {% trans "Select Part" %} + {% trans "Select Supplier Part" %} {% for col in columns %} @@ -48,7 +48,7 @@ - {% add row.index 1 %} + {{ row.index }} {% for field in form.visible_fields %} diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index b3f6c1326a..039ff15e1e 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -6,6 +6,7 @@ Django views for interacting with Order app from __future__ import unicode_literals from django.db import transaction +from django.db.utils import IntegrityError from django.shortcuts import get_object_or_404 from django.core.exceptions import ValidationError from django.urls import reverse @@ -581,16 +582,147 @@ class PurchaseOrderUpload(FileManagementFormView): _("Match Supplier Parts"), ] + def get_order(self): + """ Get order or return 404 """ + + return get_object_or_404(PurchaseOrder, pk=self.kwargs['pk']) + def get_context_data(self, form, **kwargs): context = super().get_context_data(form=form, **kwargs) - order = get_object_or_404(PurchaseOrder, pk=self.kwargs['pk']) + order = self.get_order() context.update({'order': order}) return context + def get_field_selection(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 SupplierPart selection form. + """ + + order = self.get_order() + + self.allowed_items = SupplierPart.objects.filter(supplier=order.supplier).prefetch_related('manufacturer_part') + + # Fields prefixed with "Part_" can be used to do "smart matching" against Part objects in the database + q_idx = self.get_column_index('Quantity') + s_idx = self.get_column_index('Supplier_SKU') + m_idx = self.get_column_index('Manufacturer_MPN') + # p_idx = self.get_column_index('Unit_Price') + # e_idx = self.get_column_index('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 + + # 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]['cell'] + + try: + # Attempt SupplierPart lookup based on SKU value + exact_match_part = self.allowed_items.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: + mpn = row['data'][m_idx]['cell'] + + try: + # Attempt SupplierPart lookup based on MPN value + exact_match_part = self.allowed_items.get(manufacturer_part__MPN__contains=mpn) + except (ValueError, SupplierPart.DoesNotExist, SupplierPart.MultipleObjectsReturned): + exact_match_part = None + + # Supply list of part options for each row, sorted by how closely they match the part name + row['item_options'] = self.allowed_items + + # 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 done(self, form_list, **kwargs): + """ Once all the data is in, process it to add SupplierPart items to the order """ + + order = self.get_order() + + items = {} + + for form_key, form_value in self.get_all_cleaned_data().items(): + # Split key from row value + try: + (field, idx) = form_key.split('-') + except ValueError: + continue + + if field == self.key_item_select: + if idx not in items: + # Insert into items + items.update({ + idx: { + 'field': form_value, + } + }) + else: + # Update items + items[idx]['field'] = form_value + + if field == self.key_quantity_select: + if idx not in items: + # Insert into items + items.update({ + idx: { + 'quantity': form_value, + } + }) + else: + # Update items + items[idx]['quantity'] = form_value + + # Create PurchaseOrderLineItem instances + for purchase_order_item in items.values(): + try: + supplier_part = SupplierPart.objects.get(pk=int(purchase_order_item['field'])) + except (ValueError, SupplierPart.DoesNotExist): + continue + purchase_order_line_item = PurchaseOrderLineItem( + order=order, + part=supplier_part, + quantity=purchase_order_item['quantity'], + ) + try: + purchase_order_line_item.save() + except IntegrityError: + # PurchaseOrderLineItem already exists + pass + return HttpResponseRedirect(reverse('po-detail', kwargs={'pk': self.kwargs['pk']}))