class SOLineItemSerializer(InvenTreeModelSerializer): """ Serializer for a SalesOrderLineItem object """ def __init__(self, *args, **kwargs): part_detail = kwargs.pop('part_detail', False) order_detail = kwargs.pop('order_detail', False) allocations = kwargs.pop('allocations', False) super().__init__(*args, **kwargs) if part_detail is not True: self.fields.pop('part_detail') if order_detail is not True: self.fields.pop('order_detail') if allocations is not True: self.fields.pop('allocations') order_detail = SalesOrderSerializer(source='order', many=False, read_only=True) part_detail = PartBriefSerializer(source='part', many=False, read_only=True) allocations = SalesOrderAllocationSerializer(many=True, read_only=True, location_detail=True) quantity = InvenTreeDecimalField() allocated = serializers.FloatField(source='allocated_quantity', read_only=True) shipped = InvenTreeDecimalField(read_only=True) sale_price = InvenTreeMoneySerializer( allow_null=True ) sale_price_string = serializers.CharField(source='sale_price', read_only=True) sale_price_currency = serializers.ChoiceField( choices=currency_code_mappings(), help_text=_('Sale price currency'), ) class Meta: model = order.models.SalesOrderLineItem fields = [ 'pk', 'allocated', 'allocations', 'quantity', 'reference', 'notes', 'order', 'order_detail', 'part', 'part_detail', 'sale_price', 'sale_price_currency', 'sale_price_string', 'shipped', ]
class StockItemSerializerBrief(InvenTree.serializers.InvenTreeModelSerializer): """ Brief serializers for a StockItem """ location_name = serializers.CharField(source='location', read_only=True) part_name = serializers.CharField(source='part.full_name', read_only=True) quantity = InvenTreeDecimalField() class Meta: model = StockItem fields = [ 'part', 'part_name', 'pk', 'location', 'location_name', 'quantity', 'serial', 'supplier_part', 'uid', ] def validate_serial(self, value): if extract_int(value) > 2147483647: raise serializers.ValidationError('serial is to to big') return value
class SupplierPriceBreakSerializer(InvenTreeModelSerializer): """ Serializer for SupplierPriceBreak object """ quantity = InvenTreeDecimalField() price = InvenTreeMoneySerializer( allow_null=True, required=True, label=_('Price'), ) price_currency = serializers.ChoiceField( choices=currency_code_mappings(), default=currency_code_default, label=_('Currency'), ) class Meta: model = SupplierPriceBreak fields = [ 'pk', 'part', 'quantity', 'price', 'price_currency', ]
class StockItemSerializerBrief(InvenTree.serializers.InvenTreeModelSerializer): """Brief serializers for a StockItem.""" location_name = serializers.CharField(source='location', read_only=True) part_name = serializers.CharField(source='part.full_name', read_only=True) quantity = InvenTreeDecimalField() class Meta: """Metaclass options.""" model = StockItem fields = [ 'part', 'part_name', 'pk', 'location', 'location_name', 'quantity', 'serial', 'supplier_part', 'uid', ] def validate_serial(self, value): """Make sure serial is not to big.""" if abs(extract_int(value)) > 0x7fffffff: raise serializers.ValidationError(_("Serial number is too large")) return value
class PartInternalPriceSerializer(InvenTreeModelSerializer): """ Serializer for internal prices for Part model. """ quantity = InvenTreeDecimalField() price = InvenTreeMoneySerializer(allow_null=True) price_currency = serializers.ChoiceField( choices=currency_code_mappings(), default=currency_code_default, label=_('Currency'), help_text=_('Purchase currency of this stock item'), ) price_string = serializers.CharField(source='price', read_only=True) class Meta: model = PartInternalPriceBreak fields = [ 'pk', 'part', 'quantity', 'price', 'price_currency', 'price_string', ]
class BuildItemSerializer(InvenTreeModelSerializer): """Serializes a BuildItem object.""" bom_part = serializers.IntegerField(source='bom_item.sub_part.pk', read_only=True) part = serializers.IntegerField(source='stock_item.part.pk', read_only=True) location = serializers.IntegerField(source='stock_item.location.pk', read_only=True) # Extra (optional) detail fields part_detail = PartSerializer(source='stock_item.part', many=False, read_only=True) build_detail = BuildSerializer(source='build', many=False, read_only=True) stock_item_detail = StockItemSerializerBrief(source='stock_item', read_only=True) location_detail = LocationSerializer(source='stock_item.location', read_only=True) quantity = InvenTreeDecimalField() def __init__(self, *args, **kwargs): """Determine which extra details fields should be included""" build_detail = kwargs.pop('build_detail', False) part_detail = kwargs.pop('part_detail', False) location_detail = kwargs.pop('location_detail', False) super().__init__(*args, **kwargs) if not build_detail: self.fields.pop('build_detail') if not part_detail: self.fields.pop('part_detail') if not location_detail: self.fields.pop('location_detail') class Meta: """Serializer metaclass""" model = BuildItem fields = [ 'pk', 'bom_part', 'build', 'build_detail', 'install_into', 'location', 'location_detail', 'part', 'part_detail', 'stock_item', 'stock_item_detail', 'quantity' ]
class PartInternalPriceSerializer(InvenTreeModelSerializer): """ Serializer for internal prices for Part model. """ quantity = InvenTreeDecimalField() price = InvenTreeMoneySerializer(allow_null=True) price_string = serializers.CharField(source='price', read_only=True) class Meta: model = PartInternalPriceBreak fields = [ 'pk', 'part', 'quantity', 'price', 'price_string', ]
class BuildSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer): """ Serializes a Build object """ url = serializers.CharField(source='get_absolute_url', read_only=True) status_text = serializers.CharField(source='get_status_display', read_only=True) part_detail = PartBriefSerializer(source='part', many=False, read_only=True) quantity = InvenTreeDecimalField() overdue = serializers.BooleanField(required=False, read_only=True) issued_by_detail = UserSerializerBrief(source='issued_by', read_only=True) responsible_detail = OwnerSerializer(source='responsible', read_only=True) @staticmethod def annotate_queryset(queryset): """ Add custom annotations to the BuildSerializer queryset, performing database queries as efficiently as possible. The following annoted fields are added: - overdue: True if the build is outstanding *and* the completion date has past """ # Annotate a boolean 'overdue' flag queryset = queryset.annotate( overdue=Case(When( Build.OVERDUE_FILTER, then=Value(True, output_field=BooleanField()), ), default=Value(False, output_field=BooleanField()))) return queryset def __init__(self, *args, **kwargs): part_detail = kwargs.pop('part_detail', True) super().__init__(*args, **kwargs) if part_detail is not True: self.fields.pop('part_detail') class Meta: model = Build fields = [ 'pk', 'url', 'title', 'batch', 'creation_date', 'completed', 'completion_date', 'destination', 'parent', 'part', 'part_detail', 'overdue', 'reference', 'sales_order', 'quantity', 'status', 'status_text', 'target_date', 'take_from', 'notes', 'link', 'issued_by', 'issued_by_detail', 'responsible', 'responsible_detail', ] read_only_fields = [ 'completed', 'creation_date', 'completion_data', 'status', 'status_text', ]
class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer): """Serializer for a StockItem. - Includes serialization for the linked part - Includes serialization for the item location """ part = serializers.PrimaryKeyRelatedField( queryset=part_models.Part.objects.all(), many=False, allow_null=False, help_text=_("Base Part"), label=_("Part"), ) def validate_part(self, part): """Ensure the provided Part instance is valid""" if part.virtual: raise ValidationError( _("Stock item cannot be created for virtual parts")) return part def update(self, instance, validated_data): """Custom update method to pass the user information through to the instance.""" instance._user = self.context['user'] return super().update(instance, validated_data) @staticmethod def annotate_queryset(queryset): """Add some extra annotations to the queryset, performing database queries as efficiently as possible.""" queryset = queryset.prefetch_related( 'sales_order', 'purchase_order', ) # Annotate the queryset with the total allocated to sales orders queryset = queryset.annotate( allocated=Coalesce( SubquerySum('sales_order_allocations__quantity'), Decimal(0)) + Coalesce(SubquerySum('allocations__quantity'), Decimal(0))) # Annotate the queryset with the number of tracking items queryset = queryset.annotate( tracking_items=SubqueryCount('tracking_info')) # Add flag to indicate if the StockItem has expired queryset = queryset.annotate( expired=Case(When( StockItem.EXPIRED_FILTER, then=Value(True, output_field=BooleanField()), ), default=Value(False, output_field=BooleanField()))) # Add flag to indicate if the StockItem is stale stale_days = common.models.InvenTreeSetting.get_setting( 'STOCK_STALE_DAYS') stale_date = datetime.now().date() + timedelta(days=stale_days) stale_filter = StockItem.IN_STOCK_FILTER & ~Q(expiry_date=None) & Q( expiry_date__lt=stale_date) queryset = queryset.annotate(stale=Case( When( stale_filter, then=Value(True, output_field=BooleanField()), ), default=Value(False, output_field=BooleanField()), )) return queryset status_text = serializers.CharField(source='get_status_display', read_only=True) supplier_part_detail = SupplierPartSerializer(source='supplier_part', many=False, read_only=True) part_detail = PartBriefSerializer(source='part', many=False, read_only=True) location_detail = LocationBriefSerializer(source='location', many=False, read_only=True) quantity = InvenTreeDecimalField() # Annotated fields tracking_items = serializers.IntegerField(read_only=True, required=False) allocated = serializers.FloatField(required=False) expired = serializers.BooleanField(required=False, read_only=True) stale = serializers.BooleanField(required=False, read_only=True) purchase_price = InvenTree.serializers.InvenTreeMoneySerializer( label=_('Purchase Price'), max_digits=19, decimal_places=4, allow_null=True, help_text=_('Purchase price of this stock item'), ) purchase_price_currency = serializers.ChoiceField( choices=currency_code_mappings(), default=currency_code_default, label=_('Currency'), help_text=_('Purchase currency of this stock item'), ) purchase_price_string = serializers.SerializerMethodField() def get_purchase_price_string(self, obj): """Return purchase price as string.""" if obj.purchase_price: obj.purchase_price.decimal_places_display = 4 return str(obj.purchase_price) return '-' purchase_order_reference = serializers.CharField( source='purchase_order.reference', read_only=True) sales_order_reference = serializers.CharField( source='sales_order.reference', read_only=True) def __init__(self, *args, **kwargs): """Add detail fields.""" part_detail = kwargs.pop('part_detail', False) location_detail = kwargs.pop('location_detail', False) supplier_part_detail = kwargs.pop('supplier_part_detail', False) super(StockItemSerializer, self).__init__(*args, **kwargs) if part_detail is not True: self.fields.pop('part_detail') if location_detail is not True: self.fields.pop('location_detail') if supplier_part_detail is not True: self.fields.pop('supplier_part_detail') class Meta: """Metaclass options.""" model = StockItem fields = [ 'allocated', 'batch', 'belongs_to', 'build', 'customer', 'delete_on_deplete', 'expired', 'expiry_date', 'is_building', 'link', 'location', 'location_detail', 'notes', 'owner', 'packaging', 'part', 'part_detail', 'purchase_order', 'purchase_order_reference', 'pk', 'quantity', 'sales_order', 'sales_order_reference', 'serial', 'stale', 'status', 'status_text', 'stocktake_date', 'supplier_part', 'supplier_part_detail', 'tracking_items', 'uid', 'updated', 'purchase_price', 'purchase_price_currency', 'purchase_price_string', ] """ These fields are read-only in this context. They can be updated by accessing the appropriate API endpoints """ read_only_fields = [ 'allocated', 'stocktake_date', 'stocktake_user', 'updated', ]
class BomItemSerializer(InvenTreeModelSerializer): """ Serializer for BomItem object """ price_range = serializers.CharField(read_only=True) quantity = InvenTreeDecimalField(required=True) def validate_quantity(self, quantity): if quantity <= 0: raise serializers.ValidationError( _("Quantity must be greater than zero")) return quantity part = serializers.PrimaryKeyRelatedField(queryset=Part.objects.filter( assembly=True)) substitutes = BomItemSubstituteSerializer(many=True, read_only=True) part_detail = PartBriefSerializer(source='part', many=False, read_only=True) sub_part = serializers.PrimaryKeyRelatedField(queryset=Part.objects.filter( component=True)) sub_part_detail = PartBriefSerializer(source='sub_part', many=False, read_only=True) validated = serializers.BooleanField(read_only=True, source='is_line_valid') purchase_price_min = MoneyField(max_digits=19, decimal_places=4, read_only=True) purchase_price_max = MoneyField(max_digits=19, decimal_places=4, read_only=True) purchase_price_avg = serializers.SerializerMethodField() purchase_price_range = serializers.SerializerMethodField() # Annotated fields for available stock available_stock = serializers.FloatField(read_only=True) available_substitute_stock = serializers.FloatField(read_only=True) available_variant_stock = serializers.FloatField(read_only=True) def __init__(self, *args, **kwargs): # part_detail and sub_part_detail serializers are only included if requested. # This saves a bunch of database requests part_detail = kwargs.pop('part_detail', False) sub_part_detail = kwargs.pop('sub_part_detail', False) include_pricing = kwargs.pop('include_pricing', False) super(BomItemSerializer, self).__init__(*args, **kwargs) if part_detail is not True: self.fields.pop('part_detail') if sub_part_detail is not True: self.fields.pop('sub_part_detail') if not include_pricing: # Remove all pricing related fields self.fields.pop('price_range') self.fields.pop('purchase_price_min') self.fields.pop('purchase_price_max') self.fields.pop('purchase_price_avg') self.fields.pop('purchase_price_range') @staticmethod def setup_eager_loading(queryset): queryset = queryset.prefetch_related('part') queryset = queryset.prefetch_related('part__category') queryset = queryset.prefetch_related('part__stock_items') queryset = queryset.prefetch_related('sub_part') queryset = queryset.prefetch_related('sub_part__category') queryset = queryset.prefetch_related( 'sub_part__stock_items', 'sub_part__stock_items__allocations', 'sub_part__stock_items__sales_order_allocations', ) queryset = queryset.prefetch_related( 'substitutes', 'substitutes__part__stock_items', ) queryset = queryset.prefetch_related( 'sub_part__supplier_parts__pricebreaks') return queryset @staticmethod def annotate_queryset(queryset): """ Annotate the BomItem queryset with extra information: Annotations: available_stock: The amount of stock available for the sub_part Part object """ """ Construct an "available stock" quantity: available_stock = total_stock - build_order_allocations - sales_order_allocations """ build_order_filter = Q(build__status__in=BuildStatus.ACTIVE_CODES) sales_order_filter = Q( line__order__status__in=SalesOrderStatus.OPEN, shipment__shipment_date=None, ) # Calculate "total stock" for the referenced sub_part # Calculate the "build_order_allocations" for the sub_part # Note that these fields are only aliased, not annotated queryset = queryset.alias( total_stock=Coalesce( SubquerySum('sub_part__stock_items__quantity', filter=StockItem.IN_STOCK_FILTER), Decimal(0), output_field=models.DecimalField(), ), allocated_to_sales_orders=Coalesce( SubquerySum( 'sub_part__stock_items__sales_order_allocations__quantity', filter=sales_order_filter, ), Decimal(0), output_field=models.DecimalField(), ), allocated_to_build_orders=Coalesce( SubquerySum( 'sub_part__stock_items__allocations__quantity', filter=build_order_filter, ), Decimal(0), output_field=models.DecimalField(), ), ) # Calculate 'available_stock' based on previously annotated fields queryset = queryset.annotate(available_stock=ExpressionWrapper( F('total_stock') - F('allocated_to_sales_orders') - F('allocated_to_build_orders'), output_field=models.DecimalField(), )) # Extract similar information for any 'substitute' parts queryset = queryset.alias( substitute_stock=Coalesce( SubquerySum( 'substitutes__part__stock_items__quantity', filter=StockItem.IN_STOCK_FILTER, ), Decimal(0), output_field=models.DecimalField(), ), substitute_build_allocations=Coalesce( SubquerySum( 'substitutes__part__stock_items__allocations__quantity', filter=build_order_filter, ), Decimal(0), output_field=models.DecimalField(), ), substitute_sales_allocations=Coalesce( SubquerySum( 'substitutes__part__stock_items__sales_order_allocations__quantity', filter=sales_order_filter, ), Decimal(0), output_field=models.DecimalField(), ), ) # Calculate 'available_substitute_stock' field queryset = queryset.annotate( available_substitute_stock=ExpressionWrapper( F('substitute_stock') - F('substitute_build_allocations') - F('substitute_sales_allocations'), output_field=models.DecimalField(), )) # Annotate the queryset with 'available variant stock' information variant_stock_query = StockItem.objects.filter( part__tree_id=OuterRef('sub_part__tree_id'), part__lft__gt=OuterRef('sub_part__lft'), part__rght__lt=OuterRef('sub_part__rght'), ).filter(StockItem.IN_STOCK_FILTER) queryset = queryset.alias( variant_stock_total=Coalesce(Subquery( variant_stock_query.annotate(total=Func( F('quantity'), function='SUM', output_field=FloatField())).values('total')), 0, output_field=FloatField()), variant_stock_build_order_allocations=Coalesce( Subquery( variant_stock_query.annotate(total=Func( F('sales_order_allocations__quantity'), function='SUM', output_field=FloatField()), ).values('total')), 0, output_field=FloatField(), ), variant_stock_sales_order_allocations=Coalesce( Subquery( variant_stock_query.annotate(total=Func( F('allocations__quantity'), function='SUM', output_field=FloatField()), ).values('total')), 0, output_field=FloatField(), )) queryset = queryset.annotate(available_variant_stock=ExpressionWrapper( F('variant_stock_total') - F('variant_stock_build_order_allocations') - F('variant_stock_sales_order_allocations'), output_field=FloatField(), )) return queryset def get_purchase_price_range(self, obj): """ Return purchase price range """ try: purchase_price_min = obj.purchase_price_min except AttributeError: return None try: purchase_price_max = obj.purchase_price_max except AttributeError: return None if purchase_price_min and not purchase_price_max: # Get price range purchase_price_range = str(purchase_price_max) elif not purchase_price_min and purchase_price_max: # Get price range purchase_price_range = str(purchase_price_max) elif purchase_price_min and purchase_price_max: # Get price range if purchase_price_min >= purchase_price_max: # If min > max: use min only purchase_price_range = str(purchase_price_min) else: purchase_price_range = str(purchase_price_min) + " - " + str( purchase_price_max) else: purchase_price_range = '-' return purchase_price_range def get_purchase_price_avg(self, obj): """ Return purchase price average """ try: purchase_price_avg = obj.purchase_price_avg except AttributeError: return None if purchase_price_avg: # Get string representation of price average purchase_price_avg = str(purchase_price_avg) else: purchase_price_avg = '-' return purchase_price_avg class Meta: model = BomItem fields = [ 'allow_variants', 'inherited', 'note', 'optional', 'overage', 'pk', 'part', 'part_detail', 'purchase_price_avg', 'purchase_price_max', 'purchase_price_min', 'purchase_price_range', 'quantity', 'reference', 'sub_part', 'sub_part_detail', 'substitutes', 'price_range', 'validated', # Annotated fields describing available quantity 'available_stock', 'available_substitute_stock', 'available_variant_stock', ]
class BomItemSerializer(InvenTreeModelSerializer): """ Serializer for BomItem object """ price_range = serializers.CharField(read_only=True) quantity = InvenTreeDecimalField() part = serializers.PrimaryKeyRelatedField(queryset=Part.objects.filter( assembly=True)) substitutes = BomItemSubstituteSerializer(many=True, read_only=True) part_detail = PartBriefSerializer(source='part', many=False, read_only=True) sub_part = serializers.PrimaryKeyRelatedField(queryset=Part.objects.filter( component=True)) sub_part_detail = PartBriefSerializer(source='sub_part', many=False, read_only=True) validated = serializers.BooleanField(read_only=True, source='is_line_valid') purchase_price_min = MoneyField(max_digits=19, decimal_places=4, read_only=True) purchase_price_max = MoneyField(max_digits=19, decimal_places=4, read_only=True) purchase_price_avg = serializers.SerializerMethodField() purchase_price_range = serializers.SerializerMethodField() def __init__(self, *args, **kwargs): # part_detail and sub_part_detail serializers are only included if requested. # This saves a bunch of database requests part_detail = kwargs.pop('part_detail', False) sub_part_detail = kwargs.pop('sub_part_detail', False) include_pricing = kwargs.pop('include_pricing', False) super(BomItemSerializer, self).__init__(*args, **kwargs) if part_detail is not True: self.fields.pop('part_detail') if sub_part_detail is not True: self.fields.pop('sub_part_detail') if not include_pricing: # Remove all pricing related fields self.fields.pop('price_range') self.fields.pop('purchase_price_min') self.fields.pop('purchase_price_max') self.fields.pop('purchase_price_avg') self.fields.pop('purchase_price_range') @staticmethod def setup_eager_loading(queryset): queryset = queryset.prefetch_related('part') queryset = queryset.prefetch_related('part__category') queryset = queryset.prefetch_related('part__stock_items') queryset = queryset.prefetch_related('sub_part') queryset = queryset.prefetch_related('sub_part__category') queryset = queryset.prefetch_related('sub_part__stock_items') queryset = queryset.prefetch_related( 'sub_part__supplier_parts__pricebreaks') return queryset def get_purchase_price_range(self, obj): """ Return purchase price range """ try: purchase_price_min = obj.purchase_price_min except AttributeError: return None try: purchase_price_max = obj.purchase_price_max except AttributeError: return None if purchase_price_min and not purchase_price_max: # Get price range purchase_price_range = str(purchase_price_max) elif not purchase_price_min and purchase_price_max: # Get price range purchase_price_range = str(purchase_price_max) elif purchase_price_min and purchase_price_max: # Get price range if purchase_price_min >= purchase_price_max: # If min > max: use min only purchase_price_range = str(purchase_price_min) else: purchase_price_range = str(purchase_price_min) + " - " + str( purchase_price_max) else: purchase_price_range = '-' return purchase_price_range def get_purchase_price_avg(self, obj): """ Return purchase price average """ try: purchase_price_avg = obj.purchase_price_avg except AttributeError: return None if purchase_price_avg: # Get string representation of price average purchase_price_avg = str(purchase_price_avg) else: purchase_price_avg = '-' return purchase_price_avg class Meta: model = BomItem fields = [ 'allow_variants', 'inherited', 'note', 'optional', 'overage', 'pk', 'part', 'part_detail', 'purchase_price_avg', 'purchase_price_max', 'purchase_price_min', 'purchase_price_range', 'quantity', 'reference', 'sub_part', 'sub_part_detail', 'substitutes', 'price_range', 'validated', ]
class SalesOrderLineItemSerializer(InvenTreeModelSerializer): """ Serializer for a SalesOrderLineItem object """ @staticmethod def annotate_queryset(queryset): """ Add some extra annotations to this queryset: - "Overdue" status (boolean field) """ queryset = queryset.annotate(overdue=Case( When( Q(order__status__in=SalesOrderStatus.OPEN) & order.models.OrderLineItem.OVERDUE_FILTER, then=Value(True, output_field=BooleanField()), ), default=Value(False, output_field=BooleanField()), )) def __init__(self, *args, **kwargs): part_detail = kwargs.pop('part_detail', False) order_detail = kwargs.pop('order_detail', False) allocations = kwargs.pop('allocations', False) super().__init__(*args, **kwargs) if part_detail is not True: self.fields.pop('part_detail') if order_detail is not True: self.fields.pop('order_detail') if allocations is not True: self.fields.pop('allocations') order_detail = SalesOrderSerializer(source='order', many=False, read_only=True) part_detail = PartBriefSerializer(source='part', many=False, read_only=True) allocations = SalesOrderAllocationSerializer(many=True, read_only=True, location_detail=True) overdue = serializers.BooleanField(required=False, read_only=True) quantity = InvenTreeDecimalField() allocated = serializers.FloatField(source='allocated_quantity', read_only=True) shipped = InvenTreeDecimalField(read_only=True) sale_price = InvenTreeMoneySerializer(allow_null=True) sale_price_string = serializers.CharField(source='sale_price', read_only=True) sale_price_currency = serializers.ChoiceField( choices=currency_code_mappings(), help_text=_('Sale price currency'), ) class Meta: model = order.models.SalesOrderLineItem fields = [ 'pk', 'allocated', 'allocations', 'quantity', 'reference', 'notes', 'order', 'order_detail', 'overdue', 'part', 'part_detail', 'sale_price', 'sale_price_currency', 'sale_price_string', 'shipped', 'target_date', ]
class BomItemSerializer(InvenTreeModelSerializer): """Serializer for BomItem object.""" price_range = serializers.CharField(read_only=True) quantity = InvenTreeDecimalField(required=True) def validate_quantity(self, quantity): """Perform validation for the BomItem quantity field""" if quantity <= 0: raise serializers.ValidationError( _("Quantity must be greater than zero")) return quantity part = serializers.PrimaryKeyRelatedField(queryset=Part.objects.filter( assembly=True)) substitutes = BomItemSubstituteSerializer(many=True, read_only=True) part_detail = PartBriefSerializer(source='part', many=False, read_only=True) sub_part = serializers.PrimaryKeyRelatedField(queryset=Part.objects.filter( component=True)) sub_part_detail = PartBriefSerializer(source='sub_part', many=False, read_only=True) validated = serializers.BooleanField(read_only=True, source='is_line_valid') purchase_price_min = MoneyField(max_digits=19, decimal_places=4, read_only=True) purchase_price_max = MoneyField(max_digits=19, decimal_places=4, read_only=True) purchase_price_avg = serializers.SerializerMethodField() purchase_price_range = serializers.SerializerMethodField() on_order = serializers.FloatField(read_only=True) # Annotated fields for available stock available_stock = serializers.FloatField(read_only=True) available_substitute_stock = serializers.FloatField(read_only=True) available_variant_stock = serializers.FloatField(read_only=True) def __init__(self, *args, **kwargs): """Determine if extra detail fields are to be annotated on this serializer - part_detail and sub_part_detail serializers are only included if requested. - This saves a bunch of database requests """ part_detail = kwargs.pop('part_detail', False) sub_part_detail = kwargs.pop('sub_part_detail', False) include_pricing = kwargs.pop('include_pricing', False) super(BomItemSerializer, self).__init__(*args, **kwargs) if part_detail is not True: self.fields.pop('part_detail') if sub_part_detail is not True: self.fields.pop('sub_part_detail') if not include_pricing: # Remove all pricing related fields self.fields.pop('price_range') self.fields.pop('purchase_price_min') self.fields.pop('purchase_price_max') self.fields.pop('purchase_price_avg') self.fields.pop('purchase_price_range') @staticmethod def setup_eager_loading(queryset): """Prefetch against the provided queryset to speed up database access""" queryset = queryset.prefetch_related('part') queryset = queryset.prefetch_related('part__category') queryset = queryset.prefetch_related('part__stock_items') queryset = queryset.prefetch_related('sub_part') queryset = queryset.prefetch_related('sub_part__category') queryset = queryset.prefetch_related( 'sub_part__stock_items', 'sub_part__stock_items__allocations', 'sub_part__stock_items__sales_order_allocations', ) queryset = queryset.prefetch_related( 'substitutes', 'substitutes__part__stock_items', ) queryset = queryset.prefetch_related( 'sub_part__supplier_parts__pricebreaks') return queryset @staticmethod def annotate_queryset(queryset): """Annotate the BomItem queryset with extra information: Annotations: available_stock: The amount of stock available for the sub_part Part object """ """ Construct an "available stock" quantity: available_stock = total_stock - build_order_allocations - sales_order_allocations """ ref = 'sub_part__' # Annotate with the total "on order" amount for the sub-part queryset = queryset.annotate( on_order=part.filters.annotate_on_order_quantity(ref), ) # Calculate "total stock" for the referenced sub_part # Calculate the "build_order_allocations" for the sub_part # Note that these fields are only aliased, not annotated queryset = queryset.alias( total_stock=part.filters.annotate_total_stock(reference=ref), allocated_to_sales_orders=part.filters. annotate_sales_order_allocations(reference=ref), allocated_to_build_orders=part.filters. annotate_build_order_allocations(reference=ref), ) # Calculate 'available_stock' based on previously annotated fields queryset = queryset.annotate(available_stock=ExpressionWrapper( F('total_stock') - F('allocated_to_sales_orders') - F('allocated_to_build_orders'), output_field=models.DecimalField(), )) ref = 'substitutes__part__' # Extract similar information for any 'substitute' parts queryset = queryset.alias( substitute_stock=part.filters.annotate_total_stock(reference=ref), substitute_build_allocations=part.filters. annotate_build_order_allocations(reference=ref), substitute_sales_allocations=part.filters. annotate_sales_order_allocations(reference=ref)) # Calculate 'available_substitute_stock' field queryset = queryset.annotate( available_substitute_stock=ExpressionWrapper( F('substitute_stock') - F('substitute_build_allocations') - F('substitute_sales_allocations'), output_field=models.DecimalField(), )) # Annotate the queryset with 'available variant stock' information variant_stock_query = part.filters.variant_stock_query( reference='sub_part__') queryset = queryset.alias( variant_stock_total=part.filters.annotate_variant_quantity( variant_stock_query, reference='quantity'), variant_bo_allocations=part.filters.annotate_variant_quantity( variant_stock_query, reference='sales_order_allocations__quantity'), variant_so_allocations=part.filters.annotate_variant_quantity( variant_stock_query, reference='allocations__quantity'), ) queryset = queryset.annotate(available_variant_stock=ExpressionWrapper( F('variant_stock_total') - F('variant_bo_allocations') - F('variant_so_allocations'), output_field=FloatField(), )) return queryset def get_purchase_price_range(self, obj): """Return purchase price range.""" try: purchase_price_min = obj.purchase_price_min except AttributeError: return None try: purchase_price_max = obj.purchase_price_max except AttributeError: return None if purchase_price_min and not purchase_price_max: # Get price range purchase_price_range = str(purchase_price_max) elif not purchase_price_min and purchase_price_max: # Get price range purchase_price_range = str(purchase_price_max) elif purchase_price_min and purchase_price_max: # Get price range if purchase_price_min >= purchase_price_max: # If min > max: use min only purchase_price_range = str(purchase_price_min) else: purchase_price_range = str(purchase_price_min) + " - " + str( purchase_price_max) else: purchase_price_range = '-' return purchase_price_range def get_purchase_price_avg(self, obj): """Return purchase price average.""" try: purchase_price_avg = obj.purchase_price_avg except AttributeError: return None if purchase_price_avg: # Get string representation of price average purchase_price_avg = str(purchase_price_avg) else: purchase_price_avg = '-' return purchase_price_avg class Meta: """Metaclass defining serializer fields""" model = BomItem fields = [ 'allow_variants', 'inherited', 'note', 'optional', 'overage', 'pk', 'part', 'part_detail', 'purchase_price_avg', 'purchase_price_max', 'purchase_price_min', 'purchase_price_range', 'quantity', 'reference', 'sub_part', 'sub_part_detail', 'substitutes', 'price_range', 'validated', # Annotated fields describing available quantity 'available_stock', 'available_substitute_stock', 'available_variant_stock', # Annotated field describing quantity on order 'on_order', ]
class SalesOrderLineItemSerializer(InvenTreeModelSerializer): """Serializer for a SalesOrderLineItem object.""" @staticmethod def annotate_queryset(queryset): """Add some extra annotations to this queryset: - "overdue" status (boolean field) - "available_quantity" """ queryset = queryset.annotate( overdue=Case( When( Q(order__status__in=SalesOrderStatus.OPEN) & order.models.SalesOrderLineItem.OVERDUE_FILTER, then=Value(True, output_field=BooleanField()), ), default=Value(False, output_field=BooleanField()), ) ) # Annotate each line with the available stock quantity # To do this, we need to look at the total stock and any allocations queryset = queryset.alias( total_stock=part.filters.annotate_total_stock(reference='part__'), allocated_to_sales_orders=part.filters.annotate_sales_order_allocations(reference='part__'), allocated_to_build_orders=part.filters.annotate_build_order_allocations(reference='part__'), ) queryset = queryset.annotate( available_stock=ExpressionWrapper( F('total_stock') - F('allocated_to_sales_orders') - F('allocated_to_build_orders'), output_field=models.DecimalField() ) ) return queryset def __init__(self, *args, **kwargs): """Initializion routine for the serializer: - Add extra related serializer information if required """ part_detail = kwargs.pop('part_detail', False) order_detail = kwargs.pop('order_detail', False) allocations = kwargs.pop('allocations', False) super().__init__(*args, **kwargs) if part_detail is not True: self.fields.pop('part_detail') if order_detail is not True: self.fields.pop('order_detail') if allocations is not True: self.fields.pop('allocations') order_detail = SalesOrderSerializer(source='order', many=False, read_only=True) part_detail = PartBriefSerializer(source='part', many=False, read_only=True) allocations = SalesOrderAllocationSerializer(many=True, read_only=True, location_detail=True) # Annotated fields overdue = serializers.BooleanField(required=False, read_only=True) available_stock = serializers.FloatField(read_only=True) quantity = InvenTreeDecimalField() allocated = serializers.FloatField(source='allocated_quantity', read_only=True) shipped = InvenTreeDecimalField(read_only=True) sale_price = InvenTreeMoneySerializer( allow_null=True ) sale_price_string = serializers.CharField(source='sale_price', read_only=True) sale_price_currency = serializers.ChoiceField( choices=currency_code_mappings(), help_text=_('Sale price currency'), ) class Meta: """Metaclass options.""" model = order.models.SalesOrderLineItem fields = [ 'pk', 'allocated', 'allocations', 'available_stock', 'quantity', 'reference', 'notes', 'order', 'order_detail', 'overdue', 'part', 'part_detail', 'sale_price', 'sale_price_currency', 'sale_price_string', 'shipped', 'target_date', ]