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
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."""
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
- 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')
)
# Filter by 'active' status of linked supplier
supplier_active = rest_filters.BooleanFilter(
field_name='supplier__active', label=_('Supplier is Active')
)
@ -293,43 +294,48 @@ class SupplierPartFilter(rest_filters.FilterSet):
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):
"""API endpoint for list view of SupplierPart object.
# Filter by 'company' (either manufacturer or supplier)
company = rest_filters.ModelChoiceFilter(
label=_('Company'), queryset=Company.objects.all(), method='filter_company'
)
- GET: Return list of SupplierPart objects
- POST: Create a new SupplierPart object
"""
def filter_company(self, queryset, name, value):
"""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')
filterset_class = SupplierPartFilter
serializer_class = SupplierPartSerializer
def get_queryset(self, *args, **kwargs):
"""Return annotated queryest object for the SupplierPart list."""
queryset = super().get_queryset(*args, **kwargs)
queryset = SupplierPartSerializer.annotate_queryset(queryset)
return queryset
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()
queryset = queryset.prefetch_related('part', 'part__pricing_data')
return queryset
@ -351,7 +357,17 @@ class SupplierPartList(DataExportViewMixin, ListCreateDestroyAPIView):
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
@ -391,7 +407,7 @@ class SupplierPartList(DataExportViewMixin, ListCreateDestroyAPIView):
]
class SupplierPartDetail(RetrieveUpdateDestroyAPI):
class SupplierPartDetail(SupplierPartMixin, RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of SupplierPart object.
- GET: Retrieve detail view
@ -399,11 +415,6 @@ class SupplierPartDetail(RetrieveUpdateDestroyAPI):
- DELETE: Delete object
"""
queryset = SupplierPart.objects.all()
serializer_class = SupplierPartSerializer
read_only_fields = []
class SupplierPriceBreakFilter(rest_filters.FilterSet):
"""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 taggit.serializers import TagListSerializerField
import company.filters
import part.filters
import part.serializers as part_serializers
from importer.mixins import DataImportExportSerializerMixin
@ -323,6 +324,7 @@ class SupplierPartSerializer(
'availability_updated',
'description',
'in_stock',
'on_order',
'link',
'active',
'manufacturer',
@ -396,6 +398,8 @@ class SupplierPartSerializer(
# Annotated field showing total in-stock quantity
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'))
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(
on_order=company.filters.annotate_on_order_quantity()
)
return queryset
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!

View File

@ -183,10 +183,25 @@ export default function SupplierPartDetail() {
];
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',
name: 'available',
label: t`Supplier Availability`,
hidden: !data.availability_updated,
copy: true,
icon: 'packages'
},
@ -352,6 +367,28 @@ export default function SupplierPartDetail() {
label={t`Inactive`}
color='red'
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]);

View File

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