mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 12:06: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:
parent
9ab18f1da7
commit
1a8b030819
@ -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
|
||||
|
||||
|
@ -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."""
|
||||
|
36
src/backend/InvenTree/company/filters.py
Normal file
36
src/backend/InvenTree/company/filters.py
Normal 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(),
|
||||
)
|
@ -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):
|
||||
|
@ -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!
|
||||
|
||||
|
@ -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]);
|
||||
|
@ -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`
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
|
Loading…
x
Reference in New Issue
Block a user