mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
API bug fix: Distinct query (#5940)
* Fix "company" filter for StockList - distinct() was in the wrong spot - Added a new unit test to cover this * Update stocklist API filter - Move custom filtering into FilterSet class - Exposes available filters to API documentation - Improved readability / field validation * Further improvements for StockList API * For for order extra line item serializer - 'title' is not a valid field
This commit is contained in:
parent
70a96942c1
commit
65531f7611
@ -66,17 +66,16 @@ class GeneralExtraLineList(APIDownloadMixin):
|
||||
filter_backends = SEARCH_ORDER_FILTER
|
||||
|
||||
ordering_fields = [
|
||||
'title',
|
||||
'quantity',
|
||||
'note',
|
||||
'reference',
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
'title',
|
||||
'quantity',
|
||||
'note',
|
||||
'reference'
|
||||
'reference',
|
||||
'description',
|
||||
]
|
||||
|
||||
filterset_fields = [
|
||||
|
@ -629,10 +629,92 @@ class StockFilter(rest_filters.FilterSet):
|
||||
parent__in=ancestor.get_descendants(include_self=True)
|
||||
)
|
||||
|
||||
category = rest_filters.ModelChoiceFilter(
|
||||
label=_('Category'),
|
||||
queryset=PartCategory.objects.all(),
|
||||
method='filter_category'
|
||||
)
|
||||
|
||||
def filter_category(self, queryset, name, category):
|
||||
"""Filter based on part category"""
|
||||
|
||||
child_categories = category.get_descendants(include_self=True)
|
||||
|
||||
return queryset.filter(
|
||||
part__category__in=child_categories,
|
||||
)
|
||||
|
||||
bom_item = rest_filters.ModelChoiceFilter(
|
||||
label=_('BOM Item'),
|
||||
queryset=BomItem.objects.all(),
|
||||
method='filter_bom_item'
|
||||
)
|
||||
|
||||
def filter_bom_item(self, queryset, name, bom_item):
|
||||
"""Filter based on BOM item"""
|
||||
|
||||
return queryset.filter(bom_item.get_stock_filter())
|
||||
|
||||
part_tree = rest_filters.ModelChoiceFilter(
|
||||
label=_('Part Tree'),
|
||||
queryset=Part.objects.all(),
|
||||
method='filter_part_tree'
|
||||
)
|
||||
|
||||
def filter_part_tree(self, queryset, name, part_tree):
|
||||
"""Filter based on part tree"""
|
||||
return queryset.filter(
|
||||
part__tree_id=part_tree.tree_id
|
||||
)
|
||||
|
||||
company = rest_filters.ModelChoiceFilter(
|
||||
label=_('Company'),
|
||||
queryset=Company.objects.all(),
|
||||
method='filter_company'
|
||||
)
|
||||
|
||||
def filter_company(self, queryset, name, company):
|
||||
"""Filter by company (either manufacturer or supplier)"""
|
||||
return queryset.filter(
|
||||
Q(supplier_part__supplier=company) | Q(supplier_part__manufacturer_part__manufacturer=company)
|
||||
).distinct()
|
||||
|
||||
# Update date filters
|
||||
updated_before = rest_filters.DateFilter(label='Updated before', field_name='updated', lookup_expr='lte')
|
||||
updated_after = rest_filters.DateFilter(label='Updated after', field_name='updated', lookup_expr='gte')
|
||||
|
||||
# Stock "expiry" filters
|
||||
expiry_date_lte = rest_filters.DateFilter(
|
||||
label=_("Expiry date before"),
|
||||
field_name='expiry_date',
|
||||
lookup_expr='lte',
|
||||
)
|
||||
|
||||
expiry_date_gte = rest_filters.DateFilter(
|
||||
label=_('Expiry date after'),
|
||||
field_name='expiry_date',
|
||||
lookup_expr='gte',
|
||||
)
|
||||
|
||||
stale = rest_filters.BooleanFilter(label=_('Stale'), method='filter_stale')
|
||||
|
||||
def filter_stale(self, queryset, name, value):
|
||||
"""Filter by stale stock items."""
|
||||
|
||||
stale_days = common.models.InvenTreeSetting.get_setting('STOCK_STALE_DAYS')
|
||||
|
||||
if stale_days <= 0:
|
||||
# No filtering, does not make sense
|
||||
return queryset
|
||||
|
||||
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)
|
||||
|
||||
if str2bool(value):
|
||||
return queryset.filter(stale_filter)
|
||||
else:
|
||||
return queryset.exclude(stale_filter)
|
||||
|
||||
|
||||
class StockList(APIDownloadMixin, ListCreateDestroyAPIView):
|
||||
"""API endpoint for list view of Stock objects.
|
||||
@ -898,44 +980,6 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView):
|
||||
|
||||
queryset = super().filter_queryset(queryset)
|
||||
|
||||
if common.settings.stock_expiry_enabled():
|
||||
|
||||
# Filter by 'expiry date'
|
||||
expired_date_lte = params.get('expiry_date_lte', None)
|
||||
if expired_date_lte is not None:
|
||||
try:
|
||||
date_lte = datetime.fromisoformat(expired_date_lte)
|
||||
queryset = queryset.filter(expiry_date__lte=date_lte)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
expiry_date_gte = params.get('expiry_date_gte', None)
|
||||
if expiry_date_gte is not None:
|
||||
try:
|
||||
date_gte = datetime.fromisoformat(expiry_date_gte)
|
||||
queryset = queryset.filter(expiry_date__gte=date_gte)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# Filter by 'stale' status
|
||||
stale = params.get('stale', None)
|
||||
|
||||
if stale is not None:
|
||||
stale = str2bool(stale)
|
||||
|
||||
# How many days to account for "staleness"?
|
||||
stale_days = common.models.InvenTreeSetting.get_setting('STOCK_STALE_DAYS')
|
||||
|
||||
if stale_days > 0:
|
||||
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)
|
||||
|
||||
if stale:
|
||||
queryset = queryset.filter(stale_filter)
|
||||
else:
|
||||
queryset = queryset.exclude(stale_filter)
|
||||
|
||||
# Exclude stock item tree
|
||||
exclude_tree = params.get('exclude_tree', None)
|
||||
|
||||
@ -950,18 +994,6 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView):
|
||||
except (ValueError, StockItem.DoesNotExist):
|
||||
pass
|
||||
|
||||
# Filter by "part tree" - only allow parts within a given variant tree
|
||||
part_tree = params.get('part_tree', None)
|
||||
|
||||
if part_tree is not None:
|
||||
try:
|
||||
part = Part.objects.get(pk=part_tree)
|
||||
|
||||
if part.tree_id is not None:
|
||||
queryset = queryset.filter(part__tree_id=part.tree_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Exclude StockItems which are already allocated to a particular SalesOrder
|
||||
exclude_so_allocation = params.get('exclude_so_allocation', None)
|
||||
|
||||
@ -1032,37 +1064,6 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView):
|
||||
except (ValueError, StockLocation.DoesNotExist):
|
||||
pass
|
||||
|
||||
# Does the client wish to filter by part category?
|
||||
cat_id = params.get('category', None)
|
||||
|
||||
if cat_id:
|
||||
try:
|
||||
category = PartCategory.objects.get(pk=cat_id)
|
||||
queryset = queryset.filter(part__category__in=category.getUniqueChildren())
|
||||
|
||||
except (ValueError, PartCategory.DoesNotExist):
|
||||
raise ValidationError({"category": "Invalid category id specified"})
|
||||
|
||||
# Does the client wish to filter by BomItem
|
||||
bom_item_id = params.get('bom_item', None)
|
||||
|
||||
if bom_item_id is not None:
|
||||
try:
|
||||
bom_item = BomItem.objects.get(pk=bom_item_id)
|
||||
|
||||
queryset = queryset.filter(bom_item.get_stock_filter())
|
||||
|
||||
except (ValueError, BomItem.DoesNotExist):
|
||||
pass
|
||||
|
||||
# Filter by company (either manufacturer or supplier)
|
||||
company = params.get('company', None)
|
||||
|
||||
if company is not None:
|
||||
queryset = queryset.filter(
|
||||
Q(supplier_part__supplier=company) | Q(supplier_part__manufacturer_part__manufacturer=company).distinct()
|
||||
)
|
||||
|
||||
return queryset
|
||||
|
||||
filter_backends = SEARCH_ORDER_FILTER_ALIAS
|
||||
|
@ -486,6 +486,11 @@ class StockItemListTest(StockAPITestCase):
|
||||
response = self.get_stock(batch='B123')
|
||||
self.assertEqual(len(response), 1)
|
||||
|
||||
def test_filter_by_company(self):
|
||||
"""Test that we can filter stock items by company"""
|
||||
for cmp in company.models.Company.objects.all():
|
||||
self.get_stock(company=cmp.pk)
|
||||
|
||||
def test_filter_by_serialized(self):
|
||||
"""Filter StockItem by serialized status."""
|
||||
response = self.get_stock(serialized=1)
|
||||
@ -740,10 +745,10 @@ class StockItemListTest(StockAPITestCase):
|
||||
def test_query_count(self):
|
||||
"""Test that the number of queries required to fetch stock items is reasonable."""
|
||||
|
||||
def get_stock(data):
|
||||
def get_stock(data, expected_status=200):
|
||||
"""Helper function to fetch stock items."""
|
||||
response = self.client.get(self.list_url, data=data)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.status_code, expected_status)
|
||||
return response.data
|
||||
|
||||
# Create a bunch of StockItem objects
|
||||
|
Loading…
Reference in New Issue
Block a user