class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer): @staticmethod def annotate_queryset(queryset): """ Add some extra annotations to this queryset: - Total price = purchase_price * quantity - "Overdue" status (boolean field) """ queryset = queryset.annotate( total_price=ExpressionWrapper(F('purchase_price') * F('quantity'), output_field=models.DecimalField())) queryset = queryset.annotate(overdue=Case( When(Q(order__status__in=PurchaseOrderStatus.OPEN) & order.models.OrderLineItem.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', False) order_detail = kwargs.pop('order_detail', False) super().__init__(*args, **kwargs) if part_detail is not True: self.fields.pop('part_detail') self.fields.pop('supplier_part_detail') if order_detail is not True: self.fields.pop('order_detail') quantity = serializers.FloatField(min_value=0, required=True) def validate_quantity(self, quantity): if quantity <= 0: raise ValidationError(_("Quantity must be greater than zero")) return quantity def validate_purchase_order(self, purchase_order): if purchase_order.status not in PurchaseOrderStatus.OPEN: raise ValidationError(_('Order is not open')) return purchase_order received = serializers.FloatField(default=0, read_only=True) overdue = serializers.BooleanField(required=False, read_only=True) total_price = serializers.FloatField(read_only=True) part_detail = PartBriefSerializer(source='get_base_part', many=False, read_only=True) supplier_part_detail = SupplierPartSerializer(source='part', many=False, read_only=True) purchase_price = InvenTreeMoneySerializer(allow_null=True) purchase_price_string = serializers.CharField(source='purchase_price', read_only=True) destination_detail = stock.serializers.LocationBriefSerializer( source='get_destination', read_only=True) purchase_price_currency = serializers.ChoiceField( choices=currency_code_mappings(), help_text=_('Purchase price currency'), ) order_detail = PurchaseOrderSerializer(source='order', read_only=True, many=False) def validate(self, data): data = super().validate(data) supplier_part = data.get('part', None) purchase_order = data.get('order', None) if not supplier_part: raise ValidationError({ 'part': _('Supplier part must be specified'), }) if not purchase_order: raise ValidationError({ 'order': _('Purchase order must be specified'), }) # Check that the supplier part and purchase order match if supplier_part is not None and supplier_part.supplier != purchase_order.supplier: raise ValidationError({ 'part': _('Supplier must match purchase order'), 'order': _('Purchase order must match supplier'), }) return data class Meta: model = order.models.PurchaseOrderLineItem fields = [ 'pk', 'quantity', 'reference', 'notes', 'order', 'order_detail', 'overdue', 'part', 'part_detail', 'supplier_part_detail', 'received', 'purchase_price', 'purchase_price_currency', 'purchase_price_string', 'destination', 'destination_detail', 'target_date', 'total_price', ]
class StockItemSerializer(InvenTreeModelSerializer): """ Serializer for a StockItem: - Includes serialization for the linked part - Includes serialization for the item location """ @staticmethod def prefetch_queryset(queryset): """ Prefetch related database tables, to reduce database hits. """ return queryset.prefetch_related( 'belongs_to', 'build', 'customer', 'sales_order', 'supplier_part', 'supplier_part__supplier', 'supplier_part__manufacturer', 'allocations', 'sales_order_allocations', 'location', 'part', 'tracking_info', ) @staticmethod def annotate_queryset(queryset): """ Add some extra annotations to the queryset, performing database queries as efficiently as possible. """ # 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) tracking_items = serializers.IntegerField(source='tracking_info_count', read_only=True, required=False) quantity = serializers.FloatField() allocated = serializers.FloatField(source='allocation_count', required=False) expired = serializers.BooleanField(required=False, read_only=True) stale = serializers.BooleanField(required=False, read_only=True) serial = serializers.CharField(required=False) required_tests = serializers.IntegerField(source='required_test_count', read_only=True, required=False) def __init__(self, *args, **kwargs): part_detail = kwargs.pop('part_detail', False) location_detail = kwargs.pop('location_detail', False) supplier_part_detail = kwargs.pop('supplier_part_detail', False) test_detail = kwargs.pop('test_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') if test_detail is not True: self.fields.pop('required_tests') class Meta: model = StockItem fields = [ 'allocated', 'batch', 'belongs_to', 'build', 'customer', 'expired', 'expiry_date', 'in_stock', 'is_building', 'link', 'location', 'location_detail', 'notes', 'packaging', 'part', 'part_detail', 'pk', 'quantity', 'required_tests', 'sales_order', 'serial', 'stale', 'status', 'status_text', 'stocktake_date', 'supplier_part', 'supplier_part_detail', 'tracking_items', 'uid', 'updated', ] """ 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', 'in_stock' ]
class StockItemSerializer(InvenTreeModelSerializer): """ Serializer for a StockItem: - Includes serialization for the linked part - Includes serialization for the item location """ @staticmethod def prefetch_queryset(queryset): """ Prefetch related database tables, to reduce database hits. """ return queryset.prefetch_related( 'belongs_to', 'build', 'build_order', 'customer', 'sales_order', 'supplier_part', 'supplier_part__supplier', 'supplier_part__manufacturer', 'allocations', 'sales_order_allocations', 'location', 'part', 'tracking_info', ) @staticmethod def annotate_queryset(queryset): """ Add some extra annotations to the queryset, performing database queries as efficiently as possible. """ queryset = queryset.annotate( allocated=Coalesce( Sum('sales_order_allocations__quantity', distinct=True), 0) + Coalesce(Sum('allocations__quantity', distinct=True), 0), tracking_items=Count('tracking_info'), ) 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) tracking_items = serializers.IntegerField(source='tracking_info_count', read_only=True, required=False) quantity = serializers.FloatField() allocated = serializers.FloatField(source='allocation_count', required=False) serial = serializers.CharField(required=False) required_tests = serializers.IntegerField(source='required_test_count', read_only=True, required=False) def __init__(self, *args, **kwargs): part_detail = kwargs.pop('part_detail', False) location_detail = kwargs.pop('location_detail', False) supplier_part_detail = kwargs.pop('supplier_part_detail', False) test_detail = kwargs.pop('test_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') if test_detail is not True: self.fields.pop('required_tests') class Meta: model = StockItem fields = [ 'allocated', 'batch', 'belongs_to', 'customer', 'build_order', 'in_stock', 'link', 'location', 'location_detail', 'notes', 'part', 'part_detail', 'pk', 'quantity', 'required_tests', 'sales_order', 'serial', 'status', 'status_text', 'supplier_part', 'supplier_part_detail', 'tracking_items', 'uid', ] """ 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', 'in_stock' ]
class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer): """ Serializer for a StockItem: - Includes serialization for the linked part - Includes serialization for the item location """ 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. """ # 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) tracking_items = serializers.IntegerField(source='tracking_info_count', read_only=True, required=False) quantity = InvenTreeDecimalField() allocated = serializers.FloatField(source='allocation_count', required=False) expired = serializers.BooleanField(required=False, read_only=True) stale = serializers.BooleanField(required=False, read_only=True) # serial = serializers.CharField(required=False) required_tests = serializers.IntegerField(source='required_test_count', read_only=True, required=False) 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 str(obj.purchase_price) if obj.purchase_price else '-' 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): part_detail = kwargs.pop('part_detail', False) location_detail = kwargs.pop('location_detail', False) supplier_part_detail = kwargs.pop('supplier_part_detail', False) test_detail = kwargs.pop('test_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') if test_detail is not True: self.fields.pop('required_tests') class Meta: model = StockItem fields = [ 'allocated', 'batch', 'belongs_to', 'build', 'customer', 'delete_on_deplete', 'expired', 'expiry_date', 'in_stock', 'is_building', 'link', 'location', 'location_detail', 'notes', 'owner', 'packaging', 'part', 'part_detail', 'purchase_order', 'purchase_order_reference', 'pk', 'quantity', 'required_tests', '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', 'in_stock' ]
def list(self, request, *args, **kwargs): """ Override the 'list' method, as the StockLocation objects are very expensive to serialize. So, we fetch and serialize the required StockLocation objects only as required. """ queryset = self.filter_queryset(self.get_queryset()) serializer = self.get_serializer(queryset, many=True) data = serializer.data # Keep track of which related models we need to query location_ids = set() part_ids = set() supplier_part_ids = set() # Iterate through each StockItem and grab some data for item in data: loc = item['location'] if loc: location_ids.add(loc) part = item['part'] if part: part_ids.add(part) sp = item['supplier_part'] if sp: supplier_part_ids.add(sp) # Do we wish to include Part detail? if str2bool(request.query_params.get('part_detail', False)): # Fetch only the required Part objects from the database parts = Part.objects.filter(pk__in=part_ids).prefetch_related( 'category', ) part_map = {} for part in parts: part_map[part.pk] = PartBriefSerializer(part).data # Now update each StockItem with the related Part data for stock_item in data: part_id = stock_item['part'] stock_item['part_detail'] = part_map.get(part_id, None) # Do we wish to include SupplierPart detail? if str2bool(request.query_params.get('supplier_part_detail', False)): supplier_parts = SupplierPart.objects.filter(pk__in=supplier_part_ids) supplier_part_map = {} for part in supplier_parts: supplier_part_map[part.pk] = SupplierPartSerializer(part).data for stock_item in data: part_id = stock_item['supplier_part'] stock_item['supplier_part_detail'] = supplier_part_map.get(part_id, None) # Do we wish to include StockLocation detail? if str2bool(request.query_params.get('location_detail', False)): # Fetch only the required StockLocation objects from the database locations = StockLocation.objects.filter(pk__in=location_ids).prefetch_related( 'parent', 'children', ) location_map = {} # Serialize each StockLocation object for location in locations: location_map[location.pk] = LocationBriefSerializer(location).data # Now update each StockItem with the related StockLocation data for stock_item in data: loc_id = stock_item['location'] stock_item['location_detail'] = location_map.get(loc_id, None) """ Determine the response type based on the request. a) For HTTP requests (e.g. via the browseable API) return a DRF response b) For AJAX requests, simply return a JSON rendered response. Note: b) is about 100x quicker than a), because the DRF framework adds a lot of cruft """ if request.is_ajax(): return JsonResponse(data, safe=False) else: return Response(data)