Adds serializer for uploading a BOM file and extracting fields

This commit is contained in:
Oliver 2022-02-05 00:12:11 +11:00
parent 448cd18468
commit 611592694b
2 changed files with 148 additions and 0 deletions

View File

@ -1533,6 +1533,15 @@ class BomList(generics.ListCreateAPIView):
] ]
class BomExtract(generics.CreateAPIView):
"""
API endpoint for extracting BOM data from a BOM file.
"""
queryset = Part.objects.none()
serializer_class = part_serializers.BomExtractSerializer
class BomDetail(generics.RetrieveUpdateDestroyAPIView): class BomDetail(generics.RetrieveUpdateDestroyAPIView):
""" API endpoint for detail view of a single BomItem object """ """ API endpoint for detail view of a single BomItem object """
@ -1685,6 +1694,7 @@ bom_api_urls = [
url(r'^.*$', BomDetail.as_view(), name='api-bom-item-detail'), url(r'^.*$', BomDetail.as_view(), name='api-bom-item-detail'),
])), ])),
url(r'^extract/', BomExtract.as_view(), name='api-bom-extract'),
# Catch-all # Catch-all
url(r'^.*$', BomList.as_view(), name='api-bom-list'), url(r'^.*$', BomList.as_view(), name='api-bom-list'),
] ]

View File

@ -4,6 +4,8 @@ JSON serializers for Part app
import imghdr import imghdr
from decimal import Decimal from decimal import Decimal
import os
import tablib
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.db import models from django.db import models
@ -25,6 +27,7 @@ from InvenTree.serializers import (InvenTreeAttachmentSerializerField,
from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus
from stock.models import StockItem from stock.models import StockItem
from .admin import BomItemResource
from .models import (BomItem, BomItemSubstitute, from .models import (BomItem, BomItemSubstitute,
Part, PartAttachment, PartCategory, PartRelated, Part, PartAttachment, PartCategory, PartRelated,
PartParameter, PartParameterTemplate, PartSellPriceBreak, PartParameter, PartParameterTemplate, PartSellPriceBreak,
@ -699,3 +702,138 @@ class PartCopyBOMSerializer(serializers.Serializer):
skip_invalid=data.get('skip_invalid', False), skip_invalid=data.get('skip_invalid', False),
include_inherited=data.get('include_inherited', False), include_inherited=data.get('include_inherited', False),
) )
class BomExtractSerializer(serializers.Serializer):
"""
Serializer for uploading a file and extracting data from it.
Note: 2022-02-04 - This needs a *serious* refactor in future, probably
When parsing the file, the following things happen:
a) Check file format and validity
b) Look for "required" fields
c) Look for "part" fields - used to "infer" part
Once the file itself has been validated, we iterate through each data row:
- If the "level" column is provided, ignore anything below level 1
- Try to "guess" the part based on part_id / part_name / part_ipn
- Extract other fields as required
"""
bom_file = serializers.FileField(
label=_("BOM File"),
help_text=_("Select Bill of Materials file"),
required=True,
allow_empty_file=False,
)
def validate_bom_file(self, bom_file):
"""
Perform validation checks on the uploaded BOM file
"""
name, ext = os.path.splitext(bom_file.name)
# Remove the leading . from the extension
ext = ext[1:]
accepted_file_types = [
'xls', 'xlsx',
'csv', 'tsv',
'xml',
]
if ext not in accepted_file_types:
raise serializers.ValidationError(_("Unsupported file type"))
# Impose a 50MB limit on uploaded BOM files
max_upload_file_size = 50 * 1024 * 1024
if bom_file.size > max_upload_file_size:
raise serializers.ValidationError(_("File is too large"))
# Read file data into memory (bytes object)
data = bom_file.read()
if ext in ['csv', 'tsv', 'xml']:
data = data.decode()
# Convert to a tablib dataset (we expect headers)
self.dataset = tablib.Dataset().load(data, ext, headers=True)
# These columns must be present
required_columns = [
'quantity',
]
# We need at least one column to specify a "part"
part_columns = [
'part',
'part_id',
'part_name',
'part_ipn',
]
# These columns are "optional"
optional_columns = [
'allow_variants',
'inherited',
'optional',
'overage',
'note',
'reference',
]
def find_matching_column(col_name, columns):
# Direct match
if col_name in columns:
return col_name
col_name = col_name.lower().strip()
for col in columns:
if col.lower().strip() == col_name:
return col
# No match
return None
for header in required_columns:
match = find_matching_column(header, self.dataset.headers)
if match is None:
raise serializers.ValidationError(_("Missing required column") + f": '{header}'")
part_column_matches = {}
part_match = False
for col in part_columns:
col_match = find_matching_column(col, self.dataset.headers)
part_column_matches[col] = col_match
if col_match is not None:
part_match = True
if not part_match:
raise serializers.ValidationError(_("No part column found"))
return bom_file
class Meta:
fields = [
'bom_file',
]
def save(self):
"""
There is no action associated with "saving" this serializer
"""
pass