2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-17 20:45:44 +00:00

[PUI] Add BOM import tool (#7635)

* Add "field_overrides" field to DataImportSession model

* Adjust logic for extracting field value

* Add import drawer to BOM table

* Enable download of BOM data

* Improve support for hidden errors in forms

* Improve form submission on front-end

- Handle a mix of files and JSON fields
- Stringify any objects

* Update backend validation for data import session

- Accept override values if provided
- Ensure correct data format
- Update fields for BomItem serializer

* Add completion check for data import session

* Improvements to importer drawer

* Render column selection as a table

* Add debouncing to text form fields

- Significantly reduces rendering calls

* Fix for TextField

* Allow instance data to be updated manually

* Allow specification of per-field default values when importing data

* Improve rendering of import

* Improve UI for data import drawer

* Bump API version

* Add callback after bulk delete

* Update playwright test

* Fix for editRow function
This commit is contained in:
Oliver
2024-07-14 22:00:29 +10:00
committed by GitHub
parent 750e6d81fa
commit 76f8a2ee9e
19 changed files with 565 additions and 101 deletions

View File

@ -1,12 +1,15 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 221
INVENTREE_API_VERSION = 222
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v222 - 2024-07-14 : https://github.com/inventree/InvenTree/pull/7635
- Adjust the BomItem API endpoint to improve data import process
v221 - 2024-07-13 : https://github.com/inventree/InvenTree/pull/7636
- Adds missing fields from StockItemBriefSerializer
- Adds missing fields from PartBriefSerializer

View File

@ -0,0 +1,19 @@
# Generated by Django 4.2.14 on 2024-07-12 03:35
from django.db import migrations, models
import importer.validators
class Migration(migrations.Migration):
dependencies = [
('importer', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='dataimportsession',
name='field_overrides',
field=models.JSONField(blank=True, null=True, validators=[importer.validators.validate_field_defaults], verbose_name='Field Overrides'),
),
]

View File

@ -1,5 +1,6 @@
"""Model definitions for the 'importer' app."""
import json
import logging
from django.contrib.auth.models import User
@ -32,6 +33,7 @@ class DataImportSession(models.Model):
status: IntegerField for the status of the import session
user: ForeignKey to the User who initiated the import
field_defaults: JSONField for field default values
field_overrides: JSONField for field override values
"""
@staticmethod
@ -92,6 +94,13 @@ class DataImportSession(models.Model):
validators=[importer.validators.validate_field_defaults],
)
field_overrides = models.JSONField(
blank=True,
null=True,
verbose_name=_('Field Overrides'),
validators=[importer.validators.validate_field_defaults],
)
@property
def field_mapping(self):
"""Construct a dict of field mappings for this import session.
@ -132,8 +141,15 @@ class DataImportSession(models.Model):
matched_columns = set()
field_overrides = self.field_overrides or {}
# Create a default mapping for each available field in the database
for field, field_def in serializer_fields.items():
# If an override value is provided for the field,
# skip creating a mapping for this field
if field in field_overrides:
continue
# Generate a list of possible column names for this field
field_options = [
field,
@ -181,10 +197,15 @@ class DataImportSession(models.Model):
required_fields = self.required_fields()
field_defaults = self.field_defaults or {}
field_overrides = self.field_overrides or {}
missing_fields = []
for field in required_fields.keys():
# An override value exists
if field in field_overrides:
continue
# A default value exists
if field in field_defaults and field_defaults[field]:
continue
@ -265,6 +286,18 @@ class DataImportSession(models.Model):
self.status = DataImportStatusCode.PROCESSING.value
self.save()
def check_complete(self) -> bool:
"""Check if the import session is complete."""
if self.completed_row_count < self.row_count:
return False
# Update the status of this session
if self.status != DataImportStatusCode.COMPLETE.value:
self.status = DataImportStatusCode.COMPLETE.value
self.save()
return True
@property
def row_count(self):
"""Return the number of rows in the import session."""
@ -467,6 +500,34 @@ class DataImportRow(models.Model):
complete = models.BooleanField(default=False, verbose_name=_('Complete'))
@property
def default_values(self) -> dict:
"""Return a dict object of the 'default' values for this row."""
defaults = self.session.field_defaults or {}
if type(defaults) is not dict:
try:
defaults = json.loads(str(defaults))
except json.JSONDecodeError:
logger.warning('Failed to parse default values for import row')
defaults = {}
return defaults
@property
def override_values(self) -> dict:
"""Return a dict object of the 'override' values for this row."""
overrides = self.session.field_overrides or {}
if type(overrides) is not dict:
try:
overrides = json.loads(str(overrides))
except json.JSONDecodeError:
logger.warning('Failed to parse override values for import row')
overrides = {}
return overrides
def extract_data(
self, available_fields: dict = None, field_mapping: dict = None, commit=True
):
@ -477,14 +538,24 @@ class DataImportRow(models.Model):
if not available_fields:
available_fields = self.session.available_fields()
default_values = self.session.field_defaults or {}
overrride_values = self.override_values
default_values = self.default_values
data = {}
# We have mapped column (file) to field (serializer) already
for field, col in field_mapping.items():
# Data override (force value and skip any further checks)
if field in overrride_values:
data[field] = overrride_values[field]
continue
# Default value (if provided)
if field in default_values:
data[field] = default_values[field]
# If this field is *not* mapped to any column, skip
if not col:
if not col or col not in self.row_data:
continue
# Extract field type
@ -516,11 +587,14 @@ class DataImportRow(models.Model):
- If available, we use the "default" values provided by the import session
- If available, we use the "override" values provided by the import session
"""
data = self.session.field_defaults or {}
data = self.default_values
if self.data:
data.update(self.data)
# Override values take priority, if present
data.update(self.override_values)
return data
def construct_serializer(self):
@ -568,6 +642,8 @@ class DataImportRow(models.Model):
self.complete = True
self.save()
self.session.check_complete()
except Exception as e:
self.errors = {'non_field_errors': str(e)}
result = False

View File

@ -1,5 +1,7 @@
"""API serializers for the importer app."""
import json
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
@ -47,6 +49,7 @@ class DataImportSessionSerializer(InvenTreeModelSerializer):
'columns',
'column_mappings',
'field_defaults',
'field_overrides',
'row_count',
'completed_row_count',
]
@ -75,6 +78,32 @@ class DataImportSessionSerializer(InvenTreeModelSerializer):
user_detail = UserSerializer(source='user', read_only=True, many=False)
def validate_field_defaults(self, defaults):
"""De-stringify the field defaults."""
if defaults is None:
return None
if type(defaults) is not dict:
try:
defaults = json.loads(str(defaults))
except:
raise ValidationError(_('Invalid field defaults'))
return defaults
def validate_field_overrides(self, overrides):
"""De-stringify the field overrides."""
if overrides is None:
return None
if type(overrides) is not dict:
try:
overrides = json.loads(str(overrides))
except:
raise ValidationError(_('Invalid field overrides'))
return overrides
def create(self, validated_data):
"""Override create method for this serializer.
@ -167,4 +196,7 @@ class DataImportAcceptRowSerializer(serializers.Serializer):
for row in rows:
row.validate(commit=True)
if session := self.context.get('session', None):
session.check_complete()
return rows

View File

@ -1,6 +1,6 @@
"""Custom validation routines for the 'importer' app."""
import os
import json
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
@ -46,4 +46,8 @@ def validate_field_defaults(value):
return
if type(value) is not dict:
raise ValidationError(_('Value must be a valid dictionary object'))
# OK if we can parse it as JSON
try:
value = json.loads(value)
except json.JSONDecodeError:
raise ValidationError(_('Value must be a valid dictionary object'))

View File

@ -1480,28 +1480,30 @@ class BomItemSerializer(
):
"""Serializer for BomItem object."""
import_exclude_fields = ['validated', 'substitutes']
class Meta:
"""Metaclass defining serializer fields."""
model = BomItem
fields = [
'part',
'sub_part',
'reference',
'quantity',
'overage',
'allow_variants',
'inherited',
'note',
'optional',
'consumable',
'overage',
'note',
'pk',
'part',
'part_detail',
'pricing_min',
'pricing_max',
'pricing_min_total',
'pricing_max_total',
'pricing_updated',
'quantity',
'reference',
'sub_part',
'sub_part_detail',
'substitutes',
'validated',