mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 13:15:43 +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:
		
							
								
								
									
										3
									
								
								docs/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								docs/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -20,5 +20,8 @@ invoke-commands.txt | ||||
| # Temp files | ||||
| releases.json | ||||
| versions.json | ||||
| inventree_filters.yml | ||||
| inventree_settings.json | ||||
| inventree_tags.yml | ||||
|  | ||||
| .vscode/ | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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', | ||||
|         'inventreelabelmachine', | ||||
|         'inventreelabelsheet', | ||||
|         'parameter-exporter', | ||||
|     ] | ||||
|  | ||||
|     def __init__(self) -> None: | ||||
|   | ||||
| @@ -81,6 +81,8 @@ export default function InvenTreeTableHeader({ | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return filters; | ||||
|   }, [tableProps.params, tableState.filterSet, tableState.queryFilters]); | ||||
|  | ||||
|   const exportModal = useDataExport({ | ||||
|   | ||||
| @@ -269,7 +269,7 @@ export default function ParametricPartTable({ | ||||
|         tableState={table} | ||||
|         columns={tableColumns} | ||||
|         props={{ | ||||
|           enableDownload: false, | ||||
|           enableDownload: true, | ||||
|           tableFilters: tableFilters, | ||||
|           params: { | ||||
|             category: categoryId, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user