diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index b9a390d9d5..662b938d58 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -804,19 +804,31 @@ class PartFilter(rest_filters.FilterSet): Uses the django_filters extension framework """ + class Meta: + """Metaclass options for this filter set""" + model = Part + fields = [] + + has_units = rest_filters.BooleanFilter(label='Has units', method='filter_has_units') + + def filter_has_units(self, queryset, name, value): + """Filter by whether the Part has units or not""" + + if str2bool(value): + return queryset.exclude(units='') + else: + return queryset.filter(units='') + # Filter by parts which have (or not) an IPN value has_ipn = rest_filters.BooleanFilter(label='Has IPN', method='filter_has_ipn') def filter_has_ipn(self, queryset, name, value): """Filter by whether the Part has an IPN (internal part number) or not""" - value = str2bool(value) - if value: - queryset = queryset.exclude(IPN='') + if str2bool(value): + return queryset.exclude(IPN='') else: - queryset = queryset.filter(IPN='') - - return queryset + return queryset.filter(IPN='') # Regex filter for name name_regex = rest_filters.CharFilter(label='Filter by name (regex)', field_name='name', lookup_expr='iregex') @@ -836,46 +848,36 @@ class PartFilter(rest_filters.FilterSet): def filter_low_stock(self, queryset, name, value): """Filter by "low stock" status.""" - value = str2bool(value) - if value: + if str2bool(value): # Ignore any parts which do not have a specified 'minimum_stock' level - queryset = queryset.exclude(minimum_stock=0) # Filter items which have an 'in_stock' level lower than 'minimum_stock' - queryset = queryset.filter(Q(in_stock__lt=F('minimum_stock'))) + return queryset.exclude(minimum_stock=0).filter(Q(in_stock__lt=F('minimum_stock'))) else: # Filter items which have an 'in_stock' level higher than 'minimum_stock' - queryset = queryset.filter(Q(in_stock__gte=F('minimum_stock'))) - - return queryset + return queryset.filter(Q(in_stock__gte=F('minimum_stock'))) # has_stock filter has_stock = rest_filters.BooleanFilter(label='Has stock', method='filter_has_stock') def filter_has_stock(self, queryset, name, value): """Filter by whether the Part has any stock""" - value = str2bool(value) - if value: - queryset = queryset.filter(Q(in_stock__gt=0)) + if str2bool(value): + return queryset.filter(Q(in_stock__gt=0)) else: - queryset = queryset.filter(Q(in_stock__lte=0)) - - return queryset + return queryset.filter(Q(in_stock__lte=0)) # unallocated_stock filter unallocated_stock = rest_filters.BooleanFilter(label='Unallocated stock', method='filter_unallocated_stock') def filter_unallocated_stock(self, queryset, name, value): """Filter by whether the Part has unallocated stock""" - value = str2bool(value) - if value: - queryset = queryset.filter(Q(unallocated_stock__gt=0)) + if str2bool(value): + return queryset.filter(Q(unallocated_stock__gt=0)) else: - queryset = queryset.filter(Q(unallocated_stock__lte=0)) - - return queryset + return queryset.filter(Q(unallocated_stock__lte=0)) convert_from = rest_filters.ModelChoiceFilter(label="Can convert from", queryset=Part.objects.all(), method='filter_convert_from') @@ -894,9 +896,7 @@ class PartFilter(rest_filters.FilterSet): children = part.get_descendants(include_self=True) - queryset = queryset.exclude(id__in=children) - - return queryset + return queryset.exclude(id__in=children) ancestor = rest_filters.ModelChoiceFilter(label='Ancestor', queryset=Part.objects.all(), method='filter_ancestor') @@ -904,17 +904,14 @@ class PartFilter(rest_filters.FilterSet): """Limit queryset to descendants of the specified ancestor part""" descendants = part.get_descendants(include_self=False) - queryset = queryset.filter(id__in=descendants) - - return queryset + return queryset.filter(id__in=descendants) variant_of = rest_filters.ModelChoiceFilter(label='Variant Of', queryset=Part.objects.all(), method='filter_variant_of') def filter_variant_of(self, queryset, name, part): """Limit queryset to direct children (variants) of the specified part""" - queryset = queryset.filter(id__in=part.get_children()) - return queryset + return queryset.filter(id__in=part.get_children()) in_bom_for = rest_filters.ModelChoiceFilter(label='In BOM Of', queryset=Part.objects.all(), method='filter_in_bom') @@ -922,39 +919,30 @@ class PartFilter(rest_filters.FilterSet): """Limit queryset to parts in the BOM for the specified part""" bom_parts = part.get_parts_in_bom() - queryset = queryset.filter(id__in=[p.pk for p in bom_parts]) - return queryset + return queryset.filter(id__in=[p.pk for p in bom_parts]) has_pricing = rest_filters.BooleanFilter(label="Has Pricing", method="filter_has_pricing") def filter_has_pricing(self, queryset, name, value): """Filter the queryset based on whether pricing information is available for the sub_part""" - value = str2bool(value) - q_a = Q(pricing_data=None) q_b = Q(pricing_data__overall_min=None, pricing_data__overall_max=None) - if value: - queryset = queryset.exclude(q_a | q_b) + if str2bool(value): + return queryset.exclude(q_a | q_b) else: - queryset = queryset.filter(q_a | q_b) - - return queryset + return queryset.filter(q_a | q_b) stocktake = rest_filters.BooleanFilter(label="Has stocktake", method='filter_has_stocktake') def filter_has_stocktake(self, queryset, name, value): """Filter the queryset based on whether stocktake data is available""" - value = str2bool(value) - - if (value): - queryset = queryset.exclude(last_stocktake=None) + if str2bool(value): + return queryset.exclude(last_stocktake=None) else: - queryset = queryset.filter(last_stocktake=None) - - return queryset + return queryset.filter(last_stocktake=None) is_template = rest_filters.BooleanFilter() @@ -1259,6 +1247,7 @@ class PartList(PartMixin, APIDownloadMixin, ListCreateAPI): 'unallocated_stock', 'category', 'last_stocktake', + 'units', ] # Default ordering diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 9d7b76275e..98f97906c4 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -20,21 +20,11 @@ from taggit.serializers import TagListSerializerField import common.models import company.models import InvenTree.helpers +import InvenTree.serializers import InvenTree.status import part.filters import part.tasks import stock.models -from InvenTree.serializers import (DataFileExtractSerializer, - DataFileUploadSerializer, - InvenTreeAttachmentSerializer, - InvenTreeAttachmentSerializerField, - InvenTreeCurrencySerializer, - InvenTreeDecimalField, - InvenTreeImageSerializerField, - InvenTreeModelSerializer, - InvenTreeMoneySerializer, - InvenTreeTagModelSerializer, - RemoteImageMixin, UserSerializer) from InvenTree.status_codes import BuildStatus from InvenTree.tasks import offload_task @@ -48,7 +38,7 @@ from .models import (BomItem, BomItemSubstitute, Part, PartAttachment, logger = logging.getLogger("inventree") -class CategorySerializer(InvenTreeModelSerializer): +class CategorySerializer(InvenTree.serializers.InvenTreeModelSerializer): """Serializer for PartCategory.""" class Meta: @@ -94,7 +84,7 @@ class CategorySerializer(InvenTreeModelSerializer): starred = serializers.SerializerMethodField() -class CategoryTree(InvenTreeModelSerializer): +class CategoryTree(InvenTree.serializers.InvenTreeModelSerializer): """Serializer for PartCategory tree.""" class Meta: @@ -108,19 +98,19 @@ class CategoryTree(InvenTreeModelSerializer): ] -class PartAttachmentSerializer(InvenTreeAttachmentSerializer): +class PartAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSerializer): """Serializer for the PartAttachment class.""" class Meta: """Metaclass defining serializer fields""" model = PartAttachment - fields = InvenTreeAttachmentSerializer.attachment_fields([ + fields = InvenTree.serializers.InvenTreeAttachmentSerializer.attachment_fields([ 'part', ]) -class PartTestTemplateSerializer(InvenTreeModelSerializer): +class PartTestTemplateSerializer(InvenTree.serializers.InvenTreeModelSerializer): """Serializer for the PartTestTemplate class.""" class Meta: @@ -141,7 +131,7 @@ class PartTestTemplateSerializer(InvenTreeModelSerializer): key = serializers.CharField(read_only=True) -class PartSalePriceSerializer(InvenTreeModelSerializer): +class PartSalePriceSerializer(InvenTree.serializers.InvenTreeModelSerializer): """Serializer for sale prices for Part model.""" class Meta: @@ -155,14 +145,14 @@ class PartSalePriceSerializer(InvenTreeModelSerializer): 'price_currency', ] - quantity = InvenTreeDecimalField() + quantity = InvenTree.serializers.InvenTreeDecimalField() - price = InvenTreeMoneySerializer(allow_null=True) + price = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True) - price_currency = InvenTreeCurrencySerializer(help_text=_('Purchase currency of this stock item')) + price_currency = InvenTree.serializers.InvenTreeCurrencySerializer(help_text=_('Purchase currency of this stock item')) -class PartInternalPriceSerializer(InvenTreeModelSerializer): +class PartInternalPriceSerializer(InvenTree.serializers.InvenTreeModelSerializer): """Serializer for internal prices for Part model.""" class Meta: @@ -176,13 +166,13 @@ class PartInternalPriceSerializer(InvenTreeModelSerializer): 'price_currency', ] - quantity = InvenTreeDecimalField() + quantity = InvenTree.serializers.InvenTreeDecimalField() - price = InvenTreeMoneySerializer( + price = InvenTree.serializers.InvenTreeMoneySerializer( allow_null=True ) - price_currency = InvenTreeCurrencySerializer(help_text=_('Purchase currency of this stock item')) + price_currency = InvenTree.serializers.InvenTreeCurrencySerializer(help_text=_('Purchase currency of this stock item')) class PartThumbSerializer(serializers.Serializer): @@ -195,7 +185,7 @@ class PartThumbSerializer(serializers.Serializer): count = serializers.IntegerField(read_only=True) -class PartThumbSerializerUpdate(InvenTreeModelSerializer): +class PartThumbSerializerUpdate(InvenTree.serializers.InvenTreeModelSerializer): """Serializer for updating Part thumbnail.""" class Meta: @@ -212,10 +202,10 @@ class PartThumbSerializerUpdate(InvenTreeModelSerializer): raise serializers.ValidationError("File is not an image") return value - image = InvenTreeAttachmentSerializerField(required=True) + image = InvenTree.serializers.InvenTreeAttachmentSerializerField(required=True) -class PartParameterTemplateSerializer(InvenTreeModelSerializer): +class PartParameterTemplateSerializer(InvenTree.serializers.InvenTreeModelSerializer): """JSON serializer for the PartParameterTemplate model.""" class Meta: @@ -229,7 +219,7 @@ class PartParameterTemplateSerializer(InvenTreeModelSerializer): ] -class PartParameterSerializer(InvenTreeModelSerializer): +class PartParameterSerializer(InvenTree.serializers.InvenTreeModelSerializer): """JSON serializers for the PartParameter model.""" class Meta: @@ -260,7 +250,7 @@ class PartParameterSerializer(InvenTreeModelSerializer): template_detail = PartParameterTemplateSerializer(source='template', many=False, read_only=True) -class PartBriefSerializer(InvenTreeModelSerializer): +class PartBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer): """Serializer for Part (brief detail)""" class Meta: @@ -295,8 +285,8 @@ class PartBriefSerializer(InvenTreeModelSerializer): thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True) # Pricing fields - pricing_min = InvenTreeMoneySerializer(source='pricing_data.overall_min', allow_null=True, read_only=True) - pricing_max = InvenTreeMoneySerializer(source='pricing_data.overall_max', allow_null=True, read_only=True) + pricing_min = InvenTree.serializers.InvenTreeMoneySerializer(source='pricing_data.overall_min', allow_null=True, read_only=True) + pricing_max = InvenTree.serializers.InvenTreeMoneySerializer(source='pricing_data.overall_max', allow_null=True, read_only=True) class DuplicatePartSerializer(serializers.Serializer): @@ -406,7 +396,7 @@ class InitialSupplierSerializer(serializers.Serializer): return data -class PartSerializer(RemoteImageMixin, InvenTreeTagModelSerializer): +class PartSerializer(InvenTree.serializers.RemoteImageMixin, InvenTree.serializers.InvenTreeTagModelSerializer): """Serializer for complete detail information of a part. Used when displaying all details of a single component. @@ -607,7 +597,7 @@ class PartSerializer(RemoteImageMixin, InvenTreeTagModelSerializer): stock_item_count = serializers.IntegerField(read_only=True) suppliers = serializers.IntegerField(read_only=True) - image = InvenTreeImageSerializerField(required=False, allow_null=True) + image = InvenTree.serializers.InvenTreeImageSerializerField(required=False, allow_null=True) thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True) starred = serializers.SerializerMethodField() @@ -615,8 +605,8 @@ class PartSerializer(RemoteImageMixin, InvenTreeTagModelSerializer): category = serializers.PrimaryKeyRelatedField(queryset=PartCategory.objects.all()) # Pricing fields - pricing_min = InvenTreeMoneySerializer(source='pricing_data.overall_min', allow_null=True, read_only=True) - pricing_max = InvenTreeMoneySerializer(source='pricing_data.overall_max', allow_null=True, read_only=True) + pricing_min = InvenTree.serializers.InvenTreeMoneySerializer(source='pricing_data.overall_min', allow_null=True, read_only=True) + pricing_max = InvenTree.serializers.InvenTreeMoneySerializer(source='pricing_data.overall_max', allow_null=True, read_only=True) parameters = PartParameterSerializer( many=True, @@ -771,7 +761,7 @@ class PartSerializer(RemoteImageMixin, InvenTreeTagModelSerializer): return self.instance -class PartStocktakeSerializer(InvenTreeModelSerializer): +class PartStocktakeSerializer(InvenTree.serializers.InvenTreeModelSerializer): """Serializer for the PartStocktake model""" class Meta: @@ -800,13 +790,13 @@ class PartStocktakeSerializer(InvenTreeModelSerializer): quantity = serializers.FloatField() - user_detail = UserSerializer(source='user', read_only=True, many=False) + user_detail = InvenTree.serializers.UserSerializer(source='user', read_only=True, many=False) - cost_min = InvenTreeMoneySerializer(allow_null=True) - cost_min_currency = InvenTreeCurrencySerializer() + cost_min = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True) + cost_min_currency = InvenTree.serializers.InvenTreeCurrencySerializer() - cost_max = InvenTreeMoneySerializer(allow_null=True) - cost_max_currency = InvenTreeCurrencySerializer() + cost_max = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True) + cost_max_currency = InvenTree.serializers.InvenTreeCurrencySerializer() def save(self): """Called when this serializer is saved""" @@ -820,7 +810,7 @@ class PartStocktakeSerializer(InvenTreeModelSerializer): super().save() -class PartStocktakeReportSerializer(InvenTreeModelSerializer): +class PartStocktakeReportSerializer(InvenTree.serializers.InvenTreeModelSerializer): """Serializer for stocktake report class""" class Meta: @@ -836,9 +826,9 @@ class PartStocktakeReportSerializer(InvenTreeModelSerializer): 'user_detail', ] - user_detail = UserSerializer(source='user', read_only=True, many=False) + user_detail = InvenTree.serializers.UserSerializer(source='user', read_only=True, many=False) - report = InvenTreeAttachmentSerializerField(read_only=True) + report = InvenTree.serializers.InvenTreeAttachmentSerializerField(read_only=True) class PartStocktakeReportGenerateSerializer(serializers.Serializer): @@ -906,7 +896,7 @@ class PartStocktakeReportGenerateSerializer(serializers.Serializer): ) -class PartPricingSerializer(InvenTreeModelSerializer): +class PartPricingSerializer(InvenTree.serializers.InvenTreeModelSerializer): """Serializer for Part pricing information""" class Meta: @@ -942,29 +932,29 @@ class PartPricingSerializer(InvenTreeModelSerializer): scheduled_for_update = serializers.BooleanField(read_only=True) # Custom serializers - bom_cost_min = InvenTreeMoneySerializer(allow_null=True, read_only=True) - bom_cost_max = InvenTreeMoneySerializer(allow_null=True, read_only=True) + bom_cost_min = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True) + bom_cost_max = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True) - purchase_cost_min = InvenTreeMoneySerializer(allow_null=True, read_only=True) - purchase_cost_max = InvenTreeMoneySerializer(allow_null=True, read_only=True) + purchase_cost_min = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True) + purchase_cost_max = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True) - internal_cost_min = InvenTreeMoneySerializer(allow_null=True, read_only=True) - internal_cost_max = InvenTreeMoneySerializer(allow_null=True, read_only=True) + internal_cost_min = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True) + internal_cost_max = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True) - supplier_price_min = InvenTreeMoneySerializer(allow_null=True, read_only=True) - supplier_price_max = InvenTreeMoneySerializer(allow_null=True, read_only=True) + supplier_price_min = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True) + supplier_price_max = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True) - variant_cost_min = InvenTreeMoneySerializer(allow_null=True, read_only=True) - variant_cost_max = InvenTreeMoneySerializer(allow_null=True, read_only=True) + variant_cost_min = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True) + variant_cost_max = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True) - overall_min = InvenTreeMoneySerializer(allow_null=True, read_only=True) - overall_max = InvenTreeMoneySerializer(allow_null=True, read_only=True) + overall_min = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True) + overall_max = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True) - sale_price_min = InvenTreeMoneySerializer(allow_null=True, read_only=True) - sale_price_max = InvenTreeMoneySerializer(allow_null=True, read_only=True) + sale_price_min = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True) + sale_price_max = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True) - sale_history_min = InvenTreeMoneySerializer(allow_null=True, read_only=True) - sale_history_max = InvenTreeMoneySerializer(allow_null=True, read_only=True) + sale_history_min = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True) + sale_history_max = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True) update = serializers.BooleanField( write_only=True, @@ -984,7 +974,7 @@ class PartPricingSerializer(InvenTreeModelSerializer): pricing.update_pricing() -class PartRelationSerializer(InvenTreeModelSerializer): +class PartRelationSerializer(InvenTree.serializers.InvenTreeModelSerializer): """Serializer for a PartRelated model.""" class Meta: @@ -1002,7 +992,7 @@ class PartRelationSerializer(InvenTreeModelSerializer): part_2_detail = PartSerializer(source='part_2', read_only=True, many=False) -class PartStarSerializer(InvenTreeModelSerializer): +class PartStarSerializer(InvenTree.serializers.InvenTreeModelSerializer): """Serializer for a PartStar object.""" class Meta: @@ -1020,7 +1010,7 @@ class PartStarSerializer(InvenTreeModelSerializer): username = serializers.CharField(source='user.username', read_only=True) -class BomItemSubstituteSerializer(InvenTreeModelSerializer): +class BomItemSubstituteSerializer(InvenTree.serializers.InvenTreeModelSerializer): """Serializer for the BomItemSubstitute class.""" class Meta: @@ -1036,7 +1026,7 @@ class BomItemSubstituteSerializer(InvenTreeModelSerializer): part_detail = PartBriefSerializer(source='part', read_only=True, many=False) -class BomItemSerializer(InvenTreeModelSerializer): +class BomItemSerializer(InvenTree.serializers.InvenTreeModelSerializer): """Serializer for BomItem object.""" class Meta: @@ -1087,7 +1077,7 @@ class BomItemSerializer(InvenTreeModelSerializer): if sub_part_detail is not True: self.fields.pop('sub_part_detail') - quantity = InvenTreeDecimalField(required=True) + quantity = InvenTree.serializers.InvenTreeDecimalField(required=True) def validate_quantity(self, quantity): """Perform validation for the BomItem quantity field""" @@ -1109,8 +1099,8 @@ class BomItemSerializer(InvenTreeModelSerializer): on_order = serializers.FloatField(read_only=True) # Cached pricing fields - pricing_min = InvenTreeMoneySerializer(source='sub_part.pricing.overall_min', allow_null=True, read_only=True) - pricing_max = InvenTreeMoneySerializer(source='sub_part.pricing.overall_max', allow_null=True, read_only=True) + pricing_min = InvenTree.serializers.InvenTreeMoneySerializer(source='sub_part.pricing.overall_min', allow_null=True, read_only=True) + pricing_max = InvenTree.serializers.InvenTreeMoneySerializer(source='sub_part.pricing.overall_max', allow_null=True, read_only=True) # Annotated fields for available stock available_stock = serializers.FloatField(read_only=True) @@ -1212,7 +1202,7 @@ class BomItemSerializer(InvenTreeModelSerializer): return queryset -class CategoryParameterTemplateSerializer(InvenTreeModelSerializer): +class CategoryParameterTemplateSerializer(InvenTree.serializers.InvenTreeModelSerializer): """Serializer for the PartCategoryParameterTemplate model.""" class Meta: @@ -1297,7 +1287,7 @@ class PartCopyBOMSerializer(serializers.Serializer): ) -class BomImportUploadSerializer(DataFileUploadSerializer): +class BomImportUploadSerializer(InvenTree.serializers.DataFileUploadSerializer): """Serializer for uploading a file and extracting data from it.""" TARGET_MODEL = BomItem @@ -1333,7 +1323,7 @@ class BomImportUploadSerializer(DataFileUploadSerializer): part.bom_items.all().delete() -class BomImportExtractSerializer(DataFileExtractSerializer): +class BomImportExtractSerializer(InvenTree.serializers.DataFileExtractSerializer): """Serializer class for exatracting BOM data from an uploaded file. The parent class DataFileExtractSerializer does most of the heavy lifting here. diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js index 9c629caf62..06c73bc5f2 100644 --- a/InvenTree/templates/js/translated/part.js +++ b/InvenTree/templates/js/translated/part.js @@ -2038,6 +2038,12 @@ function loadPartTable(table, url, options={}) { } }); + columns.push({ + field: 'units', + title: '{% trans "Units" %}', + sortable: true, + }); + columns.push({ sortName: 'category', field: 'category_detail', diff --git a/InvenTree/templates/js/translated/table_filters.js b/InvenTree/templates/js/translated/table_filters.js index f9b925bd0f..b2567743c9 100644 --- a/InvenTree/templates/js/translated/table_filters.js +++ b/InvenTree/templates/js/translated/table_filters.js @@ -626,6 +626,11 @@ function getPartTableFilters() { type: 'bool', title: '{% trans "Component" %}', }, + has_units: { + type: 'bool', + title: '{% trans "Has Units" %}', + description: '{% trans "Part has defined units" %}', + }, has_ipn: { type: 'bool', title: '{% trans "Has IPN" %}',