mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 11:36:44 +00:00
Download params (#9413)
* Pass extra options through to data export plugins * Add PartParameterExporter plugin - Useful for including part parameter data in exporter * Fix bug in InvenTreeTableHeader * enable export for partparametertable * Simplify tests * Add typing hints
This commit is contained in:
parent
e30786b068
commit
f7a4469590
3
docs/.gitignore
vendored
3
docs/.gitignore
vendored
@ -20,5 +20,8 @@ invoke-commands.txt
|
|||||||
# Temp files
|
# Temp files
|
||||||
releases.json
|
releases.json
|
||||||
versions.json
|
versions.json
|
||||||
|
inventree_filters.yml
|
||||||
|
inventree_settings.json
|
||||||
|
inventree_tags.yml
|
||||||
|
|
||||||
.vscode/
|
.vscode/
|
||||||
|
@ -280,9 +280,14 @@ class DataExportViewMixin:
|
|||||||
# Get the base model associated with this view
|
# Get the base model associated with this view
|
||||||
try:
|
try:
|
||||||
serializer_class = self.get_serializer_class()
|
serializer_class = self.get_serializer_class()
|
||||||
|
export_kwargs['serializer_class'] = serializer_class
|
||||||
export_kwargs['model_class'] = serializer_class.Meta.model
|
export_kwargs['model_class'] = serializer_class.Meta.model
|
||||||
|
export_kwargs['view_class'] = self.__class__
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
# If the serializer class is not available, set to None
|
||||||
|
export_kwargs['serializer_class'] = None
|
||||||
export_kwargs['model_class'] = None
|
export_kwargs['model_class'] = None
|
||||||
|
export_kwargs['view_class'] = None
|
||||||
|
|
||||||
return data_exporter.serializers.DataExportOptionsSerializer(
|
return data_exporter.serializers.DataExportOptionsSerializer(
|
||||||
*args, **export_kwargs
|
*args, **export_kwargs
|
||||||
|
@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
import InvenTree.exceptions
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
import InvenTree.serializers
|
import InvenTree.serializers
|
||||||
from plugin import PluginMixinEnum, registry
|
from plugin import PluginMixinEnum, registry
|
||||||
@ -28,7 +29,10 @@ class DataExportOptionsSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
# Generate a list of plugins to choose from
|
# Generate a list of plugins to choose from
|
||||||
# If a model type is provided, use this to filter the list of plugins
|
# If a model type is provided, use this to filter the list of plugins
|
||||||
|
serializer_class = kwargs.pop('serializer_class', None)
|
||||||
model_class = kwargs.pop('model_class', None)
|
model_class = kwargs.pop('model_class', None)
|
||||||
|
view_class = kwargs.pop('view_class', None)
|
||||||
|
|
||||||
request = kwargs.pop('request', None)
|
request = kwargs.pop('request', None)
|
||||||
|
|
||||||
# Is a plugin serializer provided?
|
# Is a plugin serializer provided?
|
||||||
@ -46,7 +50,18 @@ class DataExportOptionsSerializer(serializers.Serializer):
|
|||||||
plugin_options = []
|
plugin_options = []
|
||||||
|
|
||||||
for plugin in registry.with_mixin(PluginMixinEnum.EXPORTER):
|
for plugin in registry.with_mixin(PluginMixinEnum.EXPORTER):
|
||||||
if plugin.supports_export(model_class, request.user):
|
try:
|
||||||
|
supports_export = plugin.supports_export(
|
||||||
|
model_class,
|
||||||
|
user=request.user,
|
||||||
|
serializer_class=serializer_class,
|
||||||
|
view_class=view_class,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
InvenTree.exceptions.log_error(f'plugin.{plugin.slug}.supports_export')
|
||||||
|
supports_export = False
|
||||||
|
|
||||||
|
if supports_export:
|
||||||
plugin_options.append((plugin.slug, plugin.name))
|
plugin_options.append((plugin.slug, plugin.name))
|
||||||
|
|
||||||
self.fields['export_plugin'].choices = plugin_options
|
self.fields['export_plugin'].choices = plugin_options
|
||||||
|
@ -6,7 +6,7 @@ from typing import Union
|
|||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers, views
|
||||||
|
|
||||||
from common.models import DataOutput
|
from common.models import DataOutput
|
||||||
from InvenTree.helpers import current_date
|
from InvenTree.helpers import current_date
|
||||||
@ -32,12 +32,22 @@ class DataExportMixin:
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.add_mixin(PluginMixinEnum.EXPORTER, True, __class__)
|
self.add_mixin(PluginMixinEnum.EXPORTER, True, __class__)
|
||||||
|
|
||||||
def supports_export(self, model_class: type, user: User, *args, **kwargs) -> bool:
|
def supports_export(
|
||||||
|
self,
|
||||||
|
model_class: type,
|
||||||
|
user: User,
|
||||||
|
serializer_class: serializers.Serializer = None,
|
||||||
|
view_class: views.APIView = None,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
) -> bool:
|
||||||
"""Return True if this plugin supports exporting data for the given model.
|
"""Return True if this plugin supports exporting data for the given model.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
model_class: The model class to check
|
model_class: The model class to check
|
||||||
user: The user requesting the export
|
user: The user requesting the export
|
||||||
|
serializer_class: The serializer class to use for exporting the data
|
||||||
|
view_class: The view class to use for exporting the data
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if the plugin supports exporting data for the given model
|
True if the plugin supports exporting data for the given model
|
||||||
|
@ -0,0 +1,130 @@
|
|||||||
|
"""Custom exporter for PartParameters."""
|
||||||
|
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from part.models import Part
|
||||||
|
from part.serializers import PartSerializer
|
||||||
|
from plugin import InvenTreePlugin
|
||||||
|
from plugin.mixins import DataExportMixin
|
||||||
|
|
||||||
|
|
||||||
|
class PartParameterExportOptionsSerializer(serializers.Serializer):
|
||||||
|
"""Custom export options for the PartParameterExporter plugin."""
|
||||||
|
|
||||||
|
export_stock_data = serializers.BooleanField(
|
||||||
|
default=True, label=_('Stock Data'), help_text=_('Include part stock data')
|
||||||
|
)
|
||||||
|
|
||||||
|
export_pricing_data = serializers.BooleanField(
|
||||||
|
default=True, label=_('Pricing Data'), help_text=_('Include part pricing data')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PartParameterExporter(DataExportMixin, InvenTreePlugin):
|
||||||
|
"""Builtin plugin for exporting PartParameter data.
|
||||||
|
|
||||||
|
Extends the "part" export process, to include all associated PartParameter data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
NAME = 'Part Parameter Exporter'
|
||||||
|
SLUG = 'parameter-exporter'
|
||||||
|
TITLE = _('Part Parameter Exporter')
|
||||||
|
DESCRIPTION = _('Exporter for part parameter data')
|
||||||
|
VERSION = '1.0.0'
|
||||||
|
AUTHOR = _('InvenTree contributors')
|
||||||
|
|
||||||
|
ExportOptionsSerializer = PartParameterExportOptionsSerializer
|
||||||
|
|
||||||
|
def supports_export(
|
||||||
|
self,
|
||||||
|
model_class: type,
|
||||||
|
user=None,
|
||||||
|
serializer_class=None,
|
||||||
|
view_class=None,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
) -> bool:
|
||||||
|
"""Supported if the base model is Part."""
|
||||||
|
return model_class == Part and serializer_class == PartSerializer
|
||||||
|
|
||||||
|
def update_headers(self, headers, context, **kwargs):
|
||||||
|
"""Update headers for the export."""
|
||||||
|
if not self.export_stock_data:
|
||||||
|
# Remove stock data from the headers
|
||||||
|
for field in [
|
||||||
|
'allocated_to_build_orders',
|
||||||
|
'allocated_to_sales_orders',
|
||||||
|
'available_stock',
|
||||||
|
'available_substitute_stock',
|
||||||
|
'available_variant_stock',
|
||||||
|
'building',
|
||||||
|
'can_build',
|
||||||
|
'external_stock',
|
||||||
|
'in_stock',
|
||||||
|
'on_order',
|
||||||
|
'ordering',
|
||||||
|
'required_for_build_orders',
|
||||||
|
'required_for_sales_orders',
|
||||||
|
'stock_item_count',
|
||||||
|
'total_in_stock',
|
||||||
|
'unallocated_stock',
|
||||||
|
'variant_stock',
|
||||||
|
]:
|
||||||
|
headers.pop(field, None)
|
||||||
|
|
||||||
|
if not self.export_pricing_data:
|
||||||
|
# Remove pricing data from the headers
|
||||||
|
for field in [
|
||||||
|
'pricing_min',
|
||||||
|
'pricing_max',
|
||||||
|
'pricing_min_total',
|
||||||
|
'pricing_max_total',
|
||||||
|
'pricing_updated',
|
||||||
|
]:
|
||||||
|
headers.pop(field, None)
|
||||||
|
|
||||||
|
# Add in a header for each part parameter
|
||||||
|
for pk, name in self.parameters.items():
|
||||||
|
headers[f'parameter_{pk}'] = str(name)
|
||||||
|
|
||||||
|
return headers
|
||||||
|
|
||||||
|
def prefetch_queryset(self, queryset):
|
||||||
|
"""Ensure that the part parameters are prefetched."""
|
||||||
|
queryset = queryset.prefetch_related('parameters', 'parameters__template')
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
def export_data(
|
||||||
|
self, queryset, serializer_class, headers, context, output, **kwargs
|
||||||
|
):
|
||||||
|
"""Export part and parameter data."""
|
||||||
|
# Extract custom serializer options and cache
|
||||||
|
self.export_stock_data = context.get('export_stock_data', True)
|
||||||
|
self.export_pricing_data = context.get('export_pricing_data', True)
|
||||||
|
|
||||||
|
queryset = self.prefetch_queryset(queryset)
|
||||||
|
self.serializer_class = serializer_class
|
||||||
|
|
||||||
|
# Keep a dict of observed part parameters against their primary key
|
||||||
|
self.parameters = {}
|
||||||
|
|
||||||
|
# Serialize the queryset using DRF first
|
||||||
|
parts = self.serializer_class(
|
||||||
|
queryset, parameters=True, exporting=True, many=True
|
||||||
|
).data
|
||||||
|
|
||||||
|
for part in parts:
|
||||||
|
# Extract the part parameters from the serialized data
|
||||||
|
for parameter in part.get('parameters', []):
|
||||||
|
if template := parameter.get('template_detail', None):
|
||||||
|
template_id = template['pk']
|
||||||
|
|
||||||
|
if template_id not in self.parameters:
|
||||||
|
self.parameters[template_id] = template['name']
|
||||||
|
|
||||||
|
part[f'parameter_{template_id}'] = parameter['data']
|
||||||
|
|
||||||
|
return parts
|
@ -66,6 +66,7 @@ class PluginsRegistry:
|
|||||||
'inventreelabel',
|
'inventreelabel',
|
||||||
'inventreelabelmachine',
|
'inventreelabelmachine',
|
||||||
'inventreelabelsheet',
|
'inventreelabelsheet',
|
||||||
|
'parameter-exporter',
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
@ -81,6 +81,8 @@ export default function InvenTreeTableHeader({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return filters;
|
||||||
}, [tableProps.params, tableState.filterSet, tableState.queryFilters]);
|
}, [tableProps.params, tableState.filterSet, tableState.queryFilters]);
|
||||||
|
|
||||||
const exportModal = useDataExport({
|
const exportModal = useDataExport({
|
||||||
|
@ -269,7 +269,7 @@ export default function ParametricPartTable({
|
|||||||
tableState={table}
|
tableState={table}
|
||||||
columns={tableColumns}
|
columns={tableColumns}
|
||||||
props={{
|
props={{
|
||||||
enableDownload: false,
|
enableDownload: true,
|
||||||
tableFilters: tableFilters,
|
tableFilters: tableFilters,
|
||||||
params: {
|
params: {
|
||||||
category: categoryId,
|
category: categoryId,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user