Create new file bom.py for BOM helper functions

- New class for managing BOM upload
This commit is contained in:
Oliver Walters 2019-06-27 22:46:11 +10:00
parent 65c7454124
commit 45d16f2c42
3 changed files with 102 additions and 55 deletions

View File

@ -92,33 +92,6 @@ def MakeBarcode(object_type, object_id, object_url, data={}):
return json.dumps(data, sort_keys=True) 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'): def DownloadFile(data, filename, content_type='application/text'):
""" Create a dynamic file for the user to download. """ Create a dynamic file for the user to download.

92
InvenTree/part/bom.py Normal file
View File

@ -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))})

View File

@ -8,6 +8,7 @@ from __future__ import unicode_literals
import tablib import tablib
import os import os
from django.core.exceptions import ValidationError
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.urls import reverse_lazy from django.urls import reverse_lazy
@ -23,11 +24,12 @@ from .models import match_part_names
from company.models import SupplierPart from company.models import SupplierPart
from . import forms as part_forms from . import forms as part_forms
from .bom import MakeBomTemplate, BomUploadManager
from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView
from InvenTree.views import QRCodeView from InvenTree.views import QRCodeView
from InvenTree.helpers import DownloadFile, str2bool, MakeBomTemplate from InvenTree.helpers import DownloadFile, str2bool
from InvenTree.status_codes import OrderStatus from InvenTree.status_codes import OrderStatus
@ -679,35 +681,15 @@ class BomUpload(AjaxView, FormMixin):
'form_valid': False 'form_valid': False
} }
# Extract the file format data # Create a BomUploadManager object - will perform initial data validation
ext = os.path.splitext(bom_file.name)[-1].lower() # (and raise a ValidationError if there is something wrong with the file)
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
try: 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] for k,v in errors.items():
self.form.errors[k] = v
# 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)),
]
return self.renderJsonResponse(self.request, self.form, data=data) return self.renderJsonResponse(self.request, self.form, data=data)