mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 05:05:42 +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 | # 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, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user