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

[PUI] Supplier part badges (#8625)

* API fixes for SupplierPart

- Move API filtering into SupplierPartFilter class
- Correct field annotation for detail view

* Add "in stock" and "no stock" badges to SupplierPart detail

* Update details

* Annotate 'on_order' quantity for SupplierPart

* Add "has_stock" filter to SupplierPart API

* Improve API query efficiency

* Add 'has_stock' filter to table

* Update <SupplierPartDetail>

* Bump API version
This commit is contained in:
Oliver 2024-12-03 15:21:06 +11:00 committed by GitHub
parent 9ab18f1da7
commit 1a8b030819
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 137 additions and 36 deletions

View File

@ -1,13 +1,17 @@
"""InvenTree API version information.""" """InvenTree API version information."""
# InvenTree API version # InvenTree API version
INVENTREE_API_VERSION = 291 INVENTREE_API_VERSION = 292
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" """Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """ INVENTREE_API_TEXT = """
v292 - 2024-12-03 : https://github.com/inventree/InvenTree/pull/8625
- Add "on_order" and "in_stock" annotations to SupplierPart API
- Enhanced filtering for the SupplierPart API
v291 - 2024-11-30 : https://github.com/inventree/InvenTree/pull/8596 v291 - 2024-11-30 : https://github.com/inventree/InvenTree/pull/8596
- Allow null / empty values for plugin settings - Allow null / empty values for plugin settings

View File

@ -282,6 +282,7 @@ class SupplierPartFilter(rest_filters.FilterSet):
field_name='part__active', label=_('Internal Part is Active') field_name='part__active', label=_('Internal Part is Active')
) )
# Filter by 'active' status of linked supplier
supplier_active = rest_filters.BooleanFilter( supplier_active = rest_filters.BooleanFilter(
field_name='supplier__active', label=_('Supplier is Active') field_name='supplier__active', label=_('Supplier is Active')
) )
@ -293,43 +294,48 @@ class SupplierPartFilter(rest_filters.FilterSet):
lookup_expr='iexact', lookup_expr='iexact',
) )
# Filter by 'manufacturer'
manufacturer = rest_filters.ModelChoiceFilter(
label=_('Manufacturer'),
queryset=Company.objects.all(),
field_name='manufacturer_part__manufacturer',
)
class SupplierPartList(DataExportViewMixin, ListCreateDestroyAPIView): # Filter by 'company' (either manufacturer or supplier)
"""API endpoint for list view of SupplierPart object. company = rest_filters.ModelChoiceFilter(
label=_('Company'), queryset=Company.objects.all(), method='filter_company'
)
- GET: Return list of SupplierPart objects def filter_company(self, queryset, name, value):
- POST: Create a new SupplierPart object """Filter the queryset by either manufacturer or supplier."""
""" return queryset.filter(
Q(manufacturer_part__manufacturer=value) | Q(supplier=value)
).distinct()
has_stock = rest_filters.BooleanFilter(
label=_('Has Stock'), method='filter_has_stock'
)
def filter_has_stock(self, queryset, name, value):
"""Filter the queryset based on whether the SupplierPart has stock available."""
if value:
return queryset.filter(in_stock__gt=0)
else:
return queryset.exclude(in_stock__gt=0)
class SupplierPartMixin:
"""Mixin class for SupplierPart API endpoints."""
queryset = SupplierPart.objects.all().prefetch_related('tags') queryset = SupplierPart.objects.all().prefetch_related('tags')
filterset_class = SupplierPartFilter serializer_class = SupplierPartSerializer
def get_queryset(self, *args, **kwargs): def get_queryset(self, *args, **kwargs):
"""Return annotated queryest object for the SupplierPart list.""" """Return annotated queryest object for the SupplierPart list."""
queryset = super().get_queryset(*args, **kwargs) queryset = super().get_queryset(*args, **kwargs)
queryset = SupplierPartSerializer.annotate_queryset(queryset) queryset = SupplierPartSerializer.annotate_queryset(queryset)
return queryset queryset = queryset.prefetch_related('part', 'part__pricing_data')
def filter_queryset(self, queryset):
"""Custom filtering for the queryset."""
queryset = super().filter_queryset(queryset)
params = self.request.query_params
# Filter by manufacturer
manufacturer = params.get('manufacturer', None)
if manufacturer is not None:
queryset = queryset.filter(manufacturer_part__manufacturer=manufacturer)
# Filter by EITHER manufacturer or supplier
company = params.get('company', None)
if company is not None:
queryset = queryset.filter(
Q(manufacturer_part__manufacturer=company) | Q(supplier=company)
).distinct()
return queryset return queryset
@ -351,7 +357,17 @@ class SupplierPartList(DataExportViewMixin, ListCreateDestroyAPIView):
return self.serializer_class(*args, **kwargs) return self.serializer_class(*args, **kwargs)
serializer_class = SupplierPartSerializer
class SupplierPartList(
DataExportViewMixin, SupplierPartMixin, ListCreateDestroyAPIView
):
"""API endpoint for list view of SupplierPart object.
- GET: Return list of SupplierPart objects
- POST: Create a new SupplierPart object
"""
filterset_class = SupplierPartFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS filter_backends = SEARCH_ORDER_FILTER_ALIAS
@ -391,7 +407,7 @@ class SupplierPartList(DataExportViewMixin, ListCreateDestroyAPIView):
] ]
class SupplierPartDetail(RetrieveUpdateDestroyAPI): class SupplierPartDetail(SupplierPartMixin, RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of SupplierPart object. """API endpoint for detail view of SupplierPart object.
- GET: Retrieve detail view - GET: Retrieve detail view
@ -399,11 +415,6 @@ class SupplierPartDetail(RetrieveUpdateDestroyAPI):
- DELETE: Delete object - DELETE: Delete object
""" """
queryset = SupplierPart.objects.all()
serializer_class = SupplierPartSerializer
read_only_fields = []
class SupplierPriceBreakFilter(rest_filters.FilterSet): class SupplierPriceBreakFilter(rest_filters.FilterSet):
"""Custom API filters for the SupplierPriceBreak list endpoint.""" """Custom API filters for the SupplierPriceBreak list endpoint."""

View File

@ -0,0 +1,36 @@
"""Custom query filters for the Company app."""
from decimal import Decimal
from django.db.models import DecimalField, ExpressionWrapper, F, Q
from django.db.models.functions import Coalesce
from sql_util.utils import SubquerySum
from order.status_codes import PurchaseOrderStatusGroups
def annotate_on_order_quantity():
"""Annotate the 'on_order' quantity for each SupplierPart in a queryset.
- This is the total quantity of parts on order from all open purchase orders
- Takes into account the 'received' quantity for each order line
"""
# Filter only 'active' purhase orders
# Filter only line with outstanding quantity
order_filter = Q(
order__status__in=PurchaseOrderStatusGroups.OPEN, quantity__gt=F('received')
)
return Coalesce(
SubquerySum(
ExpressionWrapper(
F('purchase_order_line_items__quantity')
- F('purchase_order_line_items__received'),
output_field=DecimalField(),
),
filter=order_filter,
),
Decimal(0),
output_field=DecimalField(),
)

View File

@ -9,6 +9,7 @@ from rest_framework import serializers
from sql_util.utils import SubqueryCount from sql_util.utils import SubqueryCount
from taggit.serializers import TagListSerializerField from taggit.serializers import TagListSerializerField
import company.filters
import part.filters import part.filters
import part.serializers as part_serializers import part.serializers as part_serializers
from importer.mixins import DataImportExportSerializerMixin from importer.mixins import DataImportExportSerializerMixin
@ -323,6 +324,7 @@ class SupplierPartSerializer(
'availability_updated', 'availability_updated',
'description', 'description',
'in_stock', 'in_stock',
'on_order',
'link', 'link',
'active', 'active',
'manufacturer', 'manufacturer',
@ -396,6 +398,8 @@ class SupplierPartSerializer(
# Annotated field showing total in-stock quantity # Annotated field showing total in-stock quantity
in_stock = serializers.FloatField(read_only=True, label=_('In Stock')) in_stock = serializers.FloatField(read_only=True, label=_('In Stock'))
on_order = serializers.FloatField(read_only=True, label=_('On Order'))
available = serializers.FloatField(required=False, label=_('Available')) available = serializers.FloatField(required=False, label=_('Available'))
pack_quantity_native = serializers.FloatField(read_only=True) pack_quantity_native = serializers.FloatField(read_only=True)
@ -442,6 +446,10 @@ class SupplierPartSerializer(
""" """
queryset = queryset.annotate(in_stock=part.filters.annotate_total_stock()) queryset = queryset.annotate(in_stock=part.filters.annotate_total_stock())
queryset = queryset.annotate(
on_order=company.filters.annotate_on_order_quantity()
)
return queryset return queryset
def update(self, supplier_part, data): def update(self, supplier_part, data):

View File

@ -1,4 +1,4 @@
"""Custom query filters for the Part models. """Custom query filters for the Part app.
The code here makes heavy use of subquery annotations! The code here makes heavy use of subquery annotations!

View File

@ -183,10 +183,25 @@ export default function SupplierPartDetail() {
]; ];
const br: DetailsField[] = [ const br: DetailsField[] = [
{
type: 'string',
name: 'in_stock',
label: t`In Stock`,
copy: true,
icon: 'stock'
},
{
type: 'string',
name: 'on_order',
label: t`On Order`,
copy: true,
icon: 'purchase_orders'
},
{ {
type: 'string', type: 'string',
name: 'available', name: 'available',
label: t`Supplier Availability`, label: t`Supplier Availability`,
hidden: !data.availability_updated,
copy: true, copy: true,
icon: 'packages' icon: 'packages'
}, },
@ -352,6 +367,28 @@ export default function SupplierPartDetail() {
label={t`Inactive`} label={t`Inactive`}
color='red' color='red'
visible={supplierPart.active == false} visible={supplierPart.active == false}
/>,
<DetailsBadge
label={`${t`In Stock`}: ${supplierPart.in_stock}`}
color={'green'}
visible={
supplierPart?.active &&
supplierPart?.in_stock &&
supplierPart?.in_stock > 0
}
key='in_stock'
/>,
<DetailsBadge
label={t`No Stock`}
color={'red'}
visible={supplierPart.active && supplierPart.in_stock == 0}
key='no_stock'
/>,
<DetailsBadge
label={`${t`On Order`}: ${supplierPart.on_order}`}
color='blue'
visible={supplierPart.on_order > 0}
key='on_order'
/> />
]; ];
}, [supplierPart]); }, [supplierPart]);

View File

@ -199,6 +199,11 @@ export function SupplierPartTable({
name: 'supplier_active', name: 'supplier_active',
label: t`Active Supplier`, label: t`Active Supplier`,
description: t`Show active suppliers` description: t`Show active suppliers`
},
{
name: 'has_stock',
label: t`In Stock`,
description: t`Show supplier parts with stock`
} }
]; ];
}, []); }, []);