Example #1
0
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',
        ]
Example #2
0
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
Example #3
0
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',
        ]
Example #4
0
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
Example #5
0
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',
        ]
Example #6
0
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'
        ]
Example #7
0
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',
        ]
Example #8
0
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',
        ]
Example #9
0
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',
        ]
Example #10
0
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',
        ]
Example #11
0
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',
        ]
Example #12
0
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',
        ]
Example #13
0
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',
        ]
Example #14
0
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',
        ]