diff --git a/docs/.gitignore b/docs/.gitignore index 3a6bc5786f..f531151dfd 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -20,5 +20,8 @@ invoke-commands.txt # Temp files releases.json versions.json +inventree_filters.yml +inventree_settings.json +inventree_tags.yml .vscode/ diff --git a/src/backend/InvenTree/data_exporter/mixins.py b/src/backend/InvenTree/data_exporter/mixins.py index 73df81906d..8274e68ee0 100644 --- a/src/backend/InvenTree/data_exporter/mixins.py +++ b/src/backend/InvenTree/data_exporter/mixins.py @@ -280,9 +280,14 @@ class DataExportViewMixin: # Get the base model associated with this view try: serializer_class = self.get_serializer_class() + export_kwargs['serializer_class'] = serializer_class export_kwargs['model_class'] = serializer_class.Meta.model + export_kwargs['view_class'] = self.__class__ except AttributeError: + # If the serializer class is not available, set to None + export_kwargs['serializer_class'] = None export_kwargs['model_class'] = None + export_kwargs['view_class'] = None return data_exporter.serializers.DataExportOptionsSerializer( *args, **export_kwargs diff --git a/src/backend/InvenTree/data_exporter/serializers.py b/src/backend/InvenTree/data_exporter/serializers.py index 214ab24973..d5f90fe48f 100644 --- a/src/backend/InvenTree/data_exporter/serializers.py +++ b/src/backend/InvenTree/data_exporter/serializers.py @@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers +import InvenTree.exceptions import InvenTree.helpers import InvenTree.serializers from plugin import PluginMixinEnum, registry @@ -28,7 +29,10 @@ class DataExportOptionsSerializer(serializers.Serializer): # Generate a list of plugins to choose from # 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) + view_class = kwargs.pop('view_class', None) + request = kwargs.pop('request', None) # Is a plugin serializer provided? @@ -46,7 +50,18 @@ class DataExportOptionsSerializer(serializers.Serializer): plugin_options = [] 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)) self.fields['export_plugin'].choices = plugin_options diff --git a/src/backend/InvenTree/plugin/base/integration/DataExport.py b/src/backend/InvenTree/plugin/base/integration/DataExport.py index 7368a5b0d7..6d1eadb6e1 100644 --- a/src/backend/InvenTree/plugin/base/integration/DataExport.py +++ b/src/backend/InvenTree/plugin/base/integration/DataExport.py @@ -6,7 +6,7 @@ from typing import Union from django.contrib.auth.models import User from django.db.models import QuerySet -from rest_framework import serializers +from rest_framework import serializers, views from common.models import DataOutput from InvenTree.helpers import current_date @@ -32,12 +32,22 @@ class DataExportMixin: super().__init__() 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. Args: model_class: The model class to check 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: True if the plugin supports exporting data for the given model diff --git a/src/backend/InvenTree/plugin/builtin/exporter/part_parameter_exporter.py b/src/backend/InvenTree/plugin/builtin/exporter/part_parameter_exporter.py new file mode 100644 index 0000000000..8ecab0e447 --- /dev/null +++ b/src/backend/InvenTree/plugin/builtin/exporter/part_parameter_exporter.py @@ -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 diff --git a/src/backend/InvenTree/plugin/registry.py b/src/backend/InvenTree/plugin/registry.py index f5debcda61..df0ff39f39 100644 --- a/src/backend/InvenTree/plugin/registry.py +++ b/src/backend/InvenTree/plugin/registry.py @@ -66,6 +66,7 @@ class PluginsRegistry: 'inventreelabel', 'inventreelabelmachine', 'inventreelabelsheet', + 'parameter-exporter', ] def __init__(self) -> None: diff --git a/src/frontend/src/tables/InvenTreeTableHeader.tsx b/src/frontend/src/tables/InvenTreeTableHeader.tsx index 39612c2321..9b28b1795c 100644 --- a/src/frontend/src/tables/InvenTreeTableHeader.tsx +++ b/src/frontend/src/tables/InvenTreeTableHeader.tsx @@ -81,6 +81,8 @@ export default function InvenTreeTableHeader({ } } } + + return filters; }, [tableProps.params, tableState.filterSet, tableState.queryFilters]); const exportModal = useDataExport({ diff --git a/src/frontend/src/tables/part/ParametricPartTable.tsx b/src/frontend/src/tables/part/ParametricPartTable.tsx index 672e997347..a3cf24ae13 100644 --- a/src/frontend/src/tables/part/ParametricPartTable.tsx +++ b/src/frontend/src/tables/part/ParametricPartTable.tsx @@ -269,7 +269,7 @@ export default function ParametricPartTable({ tableState={table} columns={tableColumns} props={{ - enableDownload: false, + enableDownload: true, tableFilters: tableFilters, params: { category: categoryId,