2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-28 03:26:45 +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:
Oliver 2025-03-31 19:45:59 +11:00 committed by GitHub
parent e30786b068
commit f7a4469590
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 170 additions and 4 deletions

3
docs/.gitignore vendored
View File

@ -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/

View File

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

View File

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

View File

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

View File

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

View File

@ -66,6 +66,7 @@ class PluginsRegistry:
'inventreelabel', 'inventreelabel',
'inventreelabelmachine', 'inventreelabelmachine',
'inventreelabelsheet', 'inventreelabelsheet',
'parameter-exporter',
] ]
def __init__(self) -> None: def __init__(self) -> None:

View File

@ -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({

View File

@ -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,