From 45d16f2c42a59631f032623f97f96bf73526b989 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 27 Jun 2019 22:46:11 +1000 Subject: [PATCH] Create new file bom.py for BOM helper functions - New class for managing BOM upload --- InvenTree/InvenTree/helpers.py | 27 ---------- InvenTree/part/bom.py | 92 ++++++++++++++++++++++++++++++++++ InvenTree/part/views.py | 38 ++++---------- 3 files changed, 102 insertions(+), 55 deletions(-) create mode 100644 InvenTree/part/bom.py diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index 2c2f74fde6..b37c73f028 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -92,33 +92,6 @@ def MakeBarcode(object_type, object_id, object_url, data={}): return json.dumps(data, sort_keys=True) -def IsValidSpreadsheetFormat(fmt): - """ Test if a file format specifier is in the valid list of spreadsheet file formats """ - - return fmt.lower() in ['csv', 'xls', 'xlsx', 'tsv'] - - -def MakeBomTemplate(fmt): - """ Generate a Bill of Materials upload template file (for user download) """ - - if not IsValidSpreadsheetFormat(fmt): - fmt = 'csv' - - fields = [ - 'Part', - 'Quantity', - 'Overage', - 'Reference', - 'Notes' - ] - - data = tablib.Dataset(headers=fields).export(fmt) - - filename = 'InvenTree_BOM_Template.' + fmt - - return DownloadFile(data, filename) - - def DownloadFile(data, filename, content_type='application/text'): """ Create a dynamic file for the user to download. diff --git a/InvenTree/part/bom.py b/InvenTree/part/bom.py new file mode 100644 index 0000000000..4f0560e259 --- /dev/null +++ b/InvenTree/part/bom.py @@ -0,0 +1,92 @@ +""" +Functionality for Bill of Material (BOM) management. +Primarily BOM upload tools. +""" + +from fuzzywuzzy import fuzz +import tablib +import os + +from django.utils.translation import gettext_lazy as _ +from django.core.exceptions import ValidationError +from .models import Part, BomItem + +from InvenTree.helpers import DownloadFile + + +def IsValidBOMFormat(fmt): + """ Test if a file format specifier is in the valid list of BOM file formats """ + + return fmt.strip().lower() in ['csv', 'xls', 'xlsx', 'tsv'] + + +def MakeBomTemplate(fmt): + """ Generate a Bill of Materials upload template file (for user download) """ + + fmt = fmt.strip().lower() + + if not IsValidBOMFormat(fmt): + fmt = 'csv' + + fields = [ + 'Part', + 'Quantity', + 'Overage', + 'Reference', + 'Notes' + ] + + data = tablib.Dataset(headers=fields).export(fmt) + + filename = 'InvenTree_BOM_Template.' + fmt + + return DownloadFile(data, filename) + + +class BomUploadManager: + """ Class for managing an uploaded BOM file """ + + # Fields which are absolutely necessary for valid upload + REQUIRED_HEADERS = [ + 'Part', + 'Quantity', + ] + + # Fields which are not necessary but can be populated + USEFUL_HEADERS = [ + 'REFERENCE', + 'OVERAGE', + 'NOTES' + ] + + def __init__(self, bom_file, starting_row=2): + """ Initialize the BomUpload class with a user-uploaded file object """ + self.starting_row = starting_row + print("Starting on row", starting_row) + self.process(bom_file) + + def process(self, bom_file): + """ Process a BOM file """ + + ext = os.path.splitext(bom_file.name)[-1].lower() + + if ext in ['.csv', '.tsv', ]: + # These file formats need string decoding + raw_data = bom_file.read().decode('utf-8') + elif ext in ['.xls', '.xlsx']: + raw_data = bom_file.read() + else: + raise ValidationError({'bom_file': _('Unsupported file format: {f}'.format(f=ext))}) + + try: + bom_data = tablib.Dataset().load(raw_data) + except tablib.UnsupportedFormat: + raise ValidationError({'bom_file': _('Error reading BOM file (invalid data)')}) + + # Now we have BOM data in memory! + + headers = [h.lower() for h in bom_data.headers] + + for header in self.REQUIRED_HEADERS: + if not header.lower() in headers: + raise ValidationError({'bom_file': _("Missing required field '{f}'".format(f=header))}) diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 3b676da4f2..11669b464b 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -8,6 +8,7 @@ from __future__ import unicode_literals import tablib import os +from django.core.exceptions import ValidationError from django.shortcuts import get_object_or_404 from django.utils.translation import gettext_lazy as _ from django.urls import reverse_lazy @@ -23,11 +24,12 @@ from .models import match_part_names from company.models import SupplierPart from . import forms as part_forms +from .bom import MakeBomTemplate, BomUploadManager from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView from InvenTree.views import QRCodeView -from InvenTree.helpers import DownloadFile, str2bool, MakeBomTemplate +from InvenTree.helpers import DownloadFile, str2bool from InvenTree.status_codes import OrderStatus @@ -679,35 +681,15 @@ class BomUpload(AjaxView, FormMixin): 'form_valid': False } - # Extract the file format data - ext = os.path.splitext(bom_file.name)[-1].lower() - - if ext in ['.csv', '.tsv', ]: - # These file formats need string decoding - raw_data = bom_file.read().decode('utf-8') - elif ext in ['.xls', '.xlsx']: - raw_data = bom_file.read() - else: - self.form.errors['bom_file'] = ['Unsupported file format: ' + ext] - return self.renderJsonResponse(self.request, self.form, data) - - # Now try to read the data + # Create a BomUploadManager object - will perform initial data validation + # (and raise a ValidationError if there is something wrong with the file) try: - bom_data = tablib.Dataset().load(raw_data) + manager = BomUploadManager(bom_file, self.form['starting_row'].value()) + except ValidationError as e: + errors = e.error_dict - headers = [h.lower() for h in bom_data.headers] - - # Minimal set of required fields - for header in ['part', 'quantity', 'reference']: - if not header in headers: - self.form.errors['bom_file'] = [_("Missing required field '{f}'".format(f=header))] - break - - except tablib.UnsupportedFormat: - valid = False - self.form.errors['bom_file'] = [ - "Error reading '{f}' (Unsupported file format)".format(f=str(bom_file)), - ] + for k,v in errors.items(): + self.form.errors[k] = v return self.renderJsonResponse(self.request, self.form, data=data)