2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-16 03:55:41 +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
"""
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

View File

@ -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.

View File

@ -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',

View File

@ -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" %}',