2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-16 20:15:44 +00:00

Display part units column in part table

- Also allow ordering by part units
- Allow filtering to show parts which have defined units
This commit is contained in:
Oliver Walters
2023-05-21 23:34:33 +10:00
parent 5268288416
commit c1182274b3
4 changed files with 110 additions and 120 deletions

View File

@ -804,19 +804,31 @@ class PartFilter(rest_filters.FilterSet):
Uses the django_filters extension framework 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 # Filter by parts which have (or not) an IPN value
has_ipn = rest_filters.BooleanFilter(label='Has IPN', method='filter_has_ipn') has_ipn = rest_filters.BooleanFilter(label='Has IPN', method='filter_has_ipn')
def filter_has_ipn(self, queryset, name, value): def filter_has_ipn(self, queryset, name, value):
"""Filter by whether the Part has an IPN (internal part number) or not""" """Filter by whether the Part has an IPN (internal part number) or not"""
value = str2bool(value)
if value: if str2bool(value):
queryset = queryset.exclude(IPN='') return queryset.exclude(IPN='')
else: else:
queryset = queryset.filter(IPN='') return queryset.filter(IPN='')
return queryset
# Regex filter for name # Regex filter for name
name_regex = rest_filters.CharFilter(label='Filter by name (regex)', field_name='name', lookup_expr='iregex') 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): def filter_low_stock(self, queryset, name, value):
"""Filter by "low stock" status.""" """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 # 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' # 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: else:
# Filter items which have an 'in_stock' level higher than 'minimum_stock' # Filter items which have an 'in_stock' level higher than 'minimum_stock'
queryset = queryset.filter(Q(in_stock__gte=F('minimum_stock'))) return queryset.filter(Q(in_stock__gte=F('minimum_stock')))
return queryset
# has_stock filter # has_stock filter
has_stock = rest_filters.BooleanFilter(label='Has stock', method='filter_has_stock') has_stock = rest_filters.BooleanFilter(label='Has stock', method='filter_has_stock')
def filter_has_stock(self, queryset, name, value): def filter_has_stock(self, queryset, name, value):
"""Filter by whether the Part has any stock""" """Filter by whether the Part has any stock"""
value = str2bool(value)
if value: if str2bool(value):
queryset = queryset.filter(Q(in_stock__gt=0)) return queryset.filter(Q(in_stock__gt=0))
else: else:
queryset = queryset.filter(Q(in_stock__lte=0)) return queryset.filter(Q(in_stock__lte=0))
return queryset
# unallocated_stock filter # unallocated_stock filter
unallocated_stock = rest_filters.BooleanFilter(label='Unallocated stock', method='filter_unallocated_stock') unallocated_stock = rest_filters.BooleanFilter(label='Unallocated stock', method='filter_unallocated_stock')
def filter_unallocated_stock(self, queryset, name, value): def filter_unallocated_stock(self, queryset, name, value):
"""Filter by whether the Part has unallocated stock""" """Filter by whether the Part has unallocated stock"""
value = str2bool(value)
if value: if str2bool(value):
queryset = queryset.filter(Q(unallocated_stock__gt=0)) return queryset.filter(Q(unallocated_stock__gt=0))
else: else:
queryset = queryset.filter(Q(unallocated_stock__lte=0)) return queryset.filter(Q(unallocated_stock__lte=0))
return queryset
convert_from = rest_filters.ModelChoiceFilter(label="Can convert from", queryset=Part.objects.all(), method='filter_convert_from') 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) children = part.get_descendants(include_self=True)
queryset = queryset.exclude(id__in=children) return queryset.exclude(id__in=children)
return queryset
ancestor = rest_filters.ModelChoiceFilter(label='Ancestor', queryset=Part.objects.all(), method='filter_ancestor') 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""" """Limit queryset to descendants of the specified ancestor part"""
descendants = part.get_descendants(include_self=False) descendants = part.get_descendants(include_self=False)
queryset = queryset.filter(id__in=descendants) return queryset.filter(id__in=descendants)
return queryset
variant_of = rest_filters.ModelChoiceFilter(label='Variant Of', queryset=Part.objects.all(), method='filter_variant_of') variant_of = rest_filters.ModelChoiceFilter(label='Variant Of', queryset=Part.objects.all(), method='filter_variant_of')
def filter_variant_of(self, queryset, name, part): def filter_variant_of(self, queryset, name, part):
"""Limit queryset to direct children (variants) of the specified part""" """Limit queryset to direct children (variants) of the specified part"""
queryset = queryset.filter(id__in=part.get_children()) return queryset.filter(id__in=part.get_children())
return queryset
in_bom_for = rest_filters.ModelChoiceFilter(label='In BOM Of', queryset=Part.objects.all(), method='filter_in_bom') 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""" """Limit queryset to parts in the BOM for the specified part"""
bom_parts = part.get_parts_in_bom() bom_parts = part.get_parts_in_bom()
queryset = queryset.filter(id__in=[p.pk for p in bom_parts]) return queryset.filter(id__in=[p.pk for p in bom_parts])
return queryset
has_pricing = rest_filters.BooleanFilter(label="Has Pricing", method="filter_has_pricing") has_pricing = rest_filters.BooleanFilter(label="Has Pricing", method="filter_has_pricing")
def filter_has_pricing(self, queryset, name, value): def filter_has_pricing(self, queryset, name, value):
"""Filter the queryset based on whether pricing information is available for the sub_part""" """Filter the queryset based on whether pricing information is available for the sub_part"""
value = str2bool(value)
q_a = Q(pricing_data=None) q_a = Q(pricing_data=None)
q_b = Q(pricing_data__overall_min=None, pricing_data__overall_max=None) q_b = Q(pricing_data__overall_min=None, pricing_data__overall_max=None)
if value: if str2bool(value):
queryset = queryset.exclude(q_a | q_b) return queryset.exclude(q_a | q_b)
else: else:
queryset = queryset.filter(q_a | q_b) return queryset.filter(q_a | q_b)
return queryset
stocktake = rest_filters.BooleanFilter(label="Has stocktake", method='filter_has_stocktake') stocktake = rest_filters.BooleanFilter(label="Has stocktake", method='filter_has_stocktake')
def filter_has_stocktake(self, queryset, name, value): def filter_has_stocktake(self, queryset, name, value):
"""Filter the queryset based on whether stocktake data is available""" """Filter the queryset based on whether stocktake data is available"""
value = str2bool(value) if str2bool(value):
return queryset.exclude(last_stocktake=None)
if (value):
queryset = queryset.exclude(last_stocktake=None)
else: else:
queryset = queryset.filter(last_stocktake=None) return queryset.filter(last_stocktake=None)
return queryset
is_template = rest_filters.BooleanFilter() is_template = rest_filters.BooleanFilter()
@ -1259,6 +1247,7 @@ class PartList(PartMixin, APIDownloadMixin, ListCreateAPI):
'unallocated_stock', 'unallocated_stock',
'category', 'category',
'last_stocktake', 'last_stocktake',
'units',
] ]
# Default ordering # Default ordering

View File

@ -20,21 +20,11 @@ from taggit.serializers import TagListSerializerField
import common.models import common.models
import company.models import company.models
import InvenTree.helpers import InvenTree.helpers
import InvenTree.serializers
import InvenTree.status import InvenTree.status
import part.filters import part.filters
import part.tasks import part.tasks
import stock.models 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.status_codes import BuildStatus
from InvenTree.tasks import offload_task from InvenTree.tasks import offload_task
@ -48,7 +38,7 @@ from .models import (BomItem, BomItemSubstitute, Part, PartAttachment,
logger = logging.getLogger("inventree") logger = logging.getLogger("inventree")
class CategorySerializer(InvenTreeModelSerializer): class CategorySerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""Serializer for PartCategory.""" """Serializer for PartCategory."""
class Meta: class Meta:
@ -94,7 +84,7 @@ class CategorySerializer(InvenTreeModelSerializer):
starred = serializers.SerializerMethodField() starred = serializers.SerializerMethodField()
class CategoryTree(InvenTreeModelSerializer): class CategoryTree(InvenTree.serializers.InvenTreeModelSerializer):
"""Serializer for PartCategory tree.""" """Serializer for PartCategory tree."""
class Meta: class Meta:
@ -108,19 +98,19 @@ class CategoryTree(InvenTreeModelSerializer):
] ]
class PartAttachmentSerializer(InvenTreeAttachmentSerializer): class PartAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSerializer):
"""Serializer for the PartAttachment class.""" """Serializer for the PartAttachment class."""
class Meta: class Meta:
"""Metaclass defining serializer fields""" """Metaclass defining serializer fields"""
model = PartAttachment model = PartAttachment
fields = InvenTreeAttachmentSerializer.attachment_fields([ fields = InvenTree.serializers.InvenTreeAttachmentSerializer.attachment_fields([
'part', 'part',
]) ])
class PartTestTemplateSerializer(InvenTreeModelSerializer): class PartTestTemplateSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""Serializer for the PartTestTemplate class.""" """Serializer for the PartTestTemplate class."""
class Meta: class Meta:
@ -141,7 +131,7 @@ class PartTestTemplateSerializer(InvenTreeModelSerializer):
key = serializers.CharField(read_only=True) key = serializers.CharField(read_only=True)
class PartSalePriceSerializer(InvenTreeModelSerializer): class PartSalePriceSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""Serializer for sale prices for Part model.""" """Serializer for sale prices for Part model."""
class Meta: class Meta:
@ -155,14 +145,14 @@ class PartSalePriceSerializer(InvenTreeModelSerializer):
'price_currency', '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.""" """Serializer for internal prices for Part model."""
class Meta: class Meta:
@ -176,13 +166,13 @@ class PartInternalPriceSerializer(InvenTreeModelSerializer):
'price_currency', 'price_currency',
] ]
quantity = InvenTreeDecimalField() quantity = InvenTree.serializers.InvenTreeDecimalField()
price = InvenTreeMoneySerializer( price = InvenTree.serializers.InvenTreeMoneySerializer(
allow_null=True 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): class PartThumbSerializer(serializers.Serializer):
@ -195,7 +185,7 @@ class PartThumbSerializer(serializers.Serializer):
count = serializers.IntegerField(read_only=True) count = serializers.IntegerField(read_only=True)
class PartThumbSerializerUpdate(InvenTreeModelSerializer): class PartThumbSerializerUpdate(InvenTree.serializers.InvenTreeModelSerializer):
"""Serializer for updating Part thumbnail.""" """Serializer for updating Part thumbnail."""
class Meta: class Meta:
@ -212,10 +202,10 @@ class PartThumbSerializerUpdate(InvenTreeModelSerializer):
raise serializers.ValidationError("File is not an image") raise serializers.ValidationError("File is not an image")
return value 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.""" """JSON serializer for the PartParameterTemplate model."""
class Meta: class Meta:
@ -229,7 +219,7 @@ class PartParameterTemplateSerializer(InvenTreeModelSerializer):
] ]
class PartParameterSerializer(InvenTreeModelSerializer): class PartParameterSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""JSON serializers for the PartParameter model.""" """JSON serializers for the PartParameter model."""
class Meta: class Meta:
@ -260,7 +250,7 @@ class PartParameterSerializer(InvenTreeModelSerializer):
template_detail = PartParameterTemplateSerializer(source='template', many=False, read_only=True) template_detail = PartParameterTemplateSerializer(source='template', many=False, read_only=True)
class PartBriefSerializer(InvenTreeModelSerializer): class PartBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""Serializer for Part (brief detail)""" """Serializer for Part (brief detail)"""
class Meta: class Meta:
@ -295,8 +285,8 @@ class PartBriefSerializer(InvenTreeModelSerializer):
thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True) thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True)
# Pricing fields # Pricing fields
pricing_min = InvenTreeMoneySerializer(source='pricing_data.overall_min', allow_null=True, read_only=True) pricing_min = InvenTree.serializers.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_max = InvenTree.serializers.InvenTreeMoneySerializer(source='pricing_data.overall_max', allow_null=True, read_only=True)
class DuplicatePartSerializer(serializers.Serializer): class DuplicatePartSerializer(serializers.Serializer):
@ -406,7 +396,7 @@ class InitialSupplierSerializer(serializers.Serializer):
return data return data
class PartSerializer(RemoteImageMixin, InvenTreeTagModelSerializer): class PartSerializer(InvenTree.serializers.RemoteImageMixin, InvenTree.serializers.InvenTreeTagModelSerializer):
"""Serializer for complete detail information of a part. """Serializer for complete detail information of a part.
Used when displaying all details of a single component. 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) stock_item_count = serializers.IntegerField(read_only=True)
suppliers = 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) thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True)
starred = serializers.SerializerMethodField() starred = serializers.SerializerMethodField()
@ -615,8 +605,8 @@ class PartSerializer(RemoteImageMixin, InvenTreeTagModelSerializer):
category = serializers.PrimaryKeyRelatedField(queryset=PartCategory.objects.all()) category = serializers.PrimaryKeyRelatedField(queryset=PartCategory.objects.all())
# Pricing fields # Pricing fields
pricing_min = InvenTreeMoneySerializer(source='pricing_data.overall_min', allow_null=True, read_only=True) pricing_min = InvenTree.serializers.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_max = InvenTree.serializers.InvenTreeMoneySerializer(source='pricing_data.overall_max', allow_null=True, read_only=True)
parameters = PartParameterSerializer( parameters = PartParameterSerializer(
many=True, many=True,
@ -771,7 +761,7 @@ class PartSerializer(RemoteImageMixin, InvenTreeTagModelSerializer):
return self.instance return self.instance
class PartStocktakeSerializer(InvenTreeModelSerializer): class PartStocktakeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""Serializer for the PartStocktake model""" """Serializer for the PartStocktake model"""
class Meta: class Meta:
@ -800,13 +790,13 @@ class PartStocktakeSerializer(InvenTreeModelSerializer):
quantity = serializers.FloatField() 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 = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True)
cost_min_currency = InvenTreeCurrencySerializer() cost_min_currency = InvenTree.serializers.InvenTreeCurrencySerializer()
cost_max = InvenTreeMoneySerializer(allow_null=True) cost_max = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True)
cost_max_currency = InvenTreeCurrencySerializer() cost_max_currency = InvenTree.serializers.InvenTreeCurrencySerializer()
def save(self): def save(self):
"""Called when this serializer is saved""" """Called when this serializer is saved"""
@ -820,7 +810,7 @@ class PartStocktakeSerializer(InvenTreeModelSerializer):
super().save() super().save()
class PartStocktakeReportSerializer(InvenTreeModelSerializer): class PartStocktakeReportSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""Serializer for stocktake report class""" """Serializer for stocktake report class"""
class Meta: class Meta:
@ -836,9 +826,9 @@ class PartStocktakeReportSerializer(InvenTreeModelSerializer):
'user_detail', '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): 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""" """Serializer for Part pricing information"""
class Meta: class Meta:
@ -942,29 +932,29 @@ class PartPricingSerializer(InvenTreeModelSerializer):
scheduled_for_update = serializers.BooleanField(read_only=True) scheduled_for_update = serializers.BooleanField(read_only=True)
# Custom serializers # Custom serializers
bom_cost_min = InvenTreeMoneySerializer(allow_null=True, read_only=True) bom_cost_min = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True)
bom_cost_max = 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_min = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True)
purchase_cost_max = 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_min = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True)
internal_cost_max = 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_min = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True)
supplier_price_max = 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_min = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True)
variant_cost_max = 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_min = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True)
overall_max = 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_min = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True)
sale_price_max = 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_min = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True)
sale_history_max = InvenTreeMoneySerializer(allow_null=True, read_only=True) sale_history_max = InvenTree.serializers.InvenTreeMoneySerializer(allow_null=True, read_only=True)
update = serializers.BooleanField( update = serializers.BooleanField(
write_only=True, write_only=True,
@ -984,7 +974,7 @@ class PartPricingSerializer(InvenTreeModelSerializer):
pricing.update_pricing() pricing.update_pricing()
class PartRelationSerializer(InvenTreeModelSerializer): class PartRelationSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""Serializer for a PartRelated model.""" """Serializer for a PartRelated model."""
class Meta: class Meta:
@ -1002,7 +992,7 @@ class PartRelationSerializer(InvenTreeModelSerializer):
part_2_detail = PartSerializer(source='part_2', read_only=True, many=False) 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.""" """Serializer for a PartStar object."""
class Meta: class Meta:
@ -1020,7 +1010,7 @@ class PartStarSerializer(InvenTreeModelSerializer):
username = serializers.CharField(source='user.username', read_only=True) username = serializers.CharField(source='user.username', read_only=True)
class BomItemSubstituteSerializer(InvenTreeModelSerializer): class BomItemSubstituteSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""Serializer for the BomItemSubstitute class.""" """Serializer for the BomItemSubstitute class."""
class Meta: class Meta:
@ -1036,7 +1026,7 @@ class BomItemSubstituteSerializer(InvenTreeModelSerializer):
part_detail = PartBriefSerializer(source='part', read_only=True, many=False) part_detail = PartBriefSerializer(source='part', read_only=True, many=False)
class BomItemSerializer(InvenTreeModelSerializer): class BomItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""Serializer for BomItem object.""" """Serializer for BomItem object."""
class Meta: class Meta:
@ -1087,7 +1077,7 @@ class BomItemSerializer(InvenTreeModelSerializer):
if sub_part_detail is not True: if sub_part_detail is not True:
self.fields.pop('sub_part_detail') self.fields.pop('sub_part_detail')
quantity = InvenTreeDecimalField(required=True) quantity = InvenTree.serializers.InvenTreeDecimalField(required=True)
def validate_quantity(self, quantity): def validate_quantity(self, quantity):
"""Perform validation for the BomItem quantity field""" """Perform validation for the BomItem quantity field"""
@ -1109,8 +1099,8 @@ class BomItemSerializer(InvenTreeModelSerializer):
on_order = serializers.FloatField(read_only=True) on_order = serializers.FloatField(read_only=True)
# Cached pricing fields # Cached pricing fields
pricing_min = InvenTreeMoneySerializer(source='sub_part.pricing.overall_min', 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 = InvenTreeMoneySerializer(source='sub_part.pricing.overall_max', 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 # Annotated fields for available stock
available_stock = serializers.FloatField(read_only=True) available_stock = serializers.FloatField(read_only=True)
@ -1212,7 +1202,7 @@ class BomItemSerializer(InvenTreeModelSerializer):
return queryset return queryset
class CategoryParameterTemplateSerializer(InvenTreeModelSerializer): class CategoryParameterTemplateSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""Serializer for the PartCategoryParameterTemplate model.""" """Serializer for the PartCategoryParameterTemplate model."""
class Meta: 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.""" """Serializer for uploading a file and extracting data from it."""
TARGET_MODEL = BomItem TARGET_MODEL = BomItem
@ -1333,7 +1323,7 @@ class BomImportUploadSerializer(DataFileUploadSerializer):
part.bom_items.all().delete() part.bom_items.all().delete()
class BomImportExtractSerializer(DataFileExtractSerializer): class BomImportExtractSerializer(InvenTree.serializers.DataFileExtractSerializer):
"""Serializer class for exatracting BOM data from an uploaded file. """Serializer class for exatracting BOM data from an uploaded file.
The parent class DataFileExtractSerializer does most of the heavy lifting here. The parent class DataFileExtractSerializer does most of the heavy lifting here.

View File

@ -2038,6 +2038,12 @@ function loadPartTable(table, url, options={}) {
} }
}); });
columns.push({
field: 'units',
title: '{% trans "Units" %}',
sortable: true,
});
columns.push({ columns.push({
sortName: 'category', sortName: 'category',
field: 'category_detail', field: 'category_detail',

View File

@ -626,6 +626,11 @@ function getPartTableFilters() {
type: 'bool', type: 'bool',
title: '{% trans "Component" %}', title: '{% trans "Component" %}',
}, },
has_units: {
type: 'bool',
title: '{% trans "Has Units" %}',
description: '{% trans "Part has defined units" %}',
},
has_ipn: { has_ipn: {
type: 'bool', type: 'bool',
title: '{% trans "Has IPN" %}', title: '{% trans "Has IPN" %}',