2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-05-22 01:06:50 +00:00

Maximum stock level (#11914)

* Add "maximum_stock" field

* Add API filter

* Update API version

* Update CHANGELOG

* Frontend updates

* Add dashboard widget

* docs

* Add API tests
This commit is contained in:
Oliver
2026-05-10 20:55:05 +10:00
committed by GitHub
parent bb2a72a6fb
commit 35aa4d33d3
14 changed files with 181 additions and 7 deletions
@@ -1,11 +1,15 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 485
INVENTREE_API_VERSION = 486
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v486 -> 2026-05-10 : https://github.com/inventree/InvenTree/pull/11914
- Adds "maximum_stock" field to the Part model and associated API endpoints
- Adds "high_stock" filter to the Part API endpoint to filter parts which are above their maximum stock level
v485 -> 2026-05-10 : https://github.com/inventree/InvenTree/pull/11631
- Adds "raw_amount" field to the BomItem API endpoint
+17
View File
@@ -729,6 +729,23 @@ class PartFilter(FilterSet):
# Filter items which have an 'in_stock' level higher than 'minimum_stock'
return queryset.filter(Q(total_in_stock__gte=F('minimum_stock')))
high_stock = rest_filters.BooleanFilter(
label='High stock', method='filter_high_stock'
)
def filter_high_stock(self, queryset, name, value):
"""Filter by "high stock" status."""
if str2bool(value):
# Ignore any parts which do not have a specified 'maximum_stock' level
# Filter items which have an 'in_stock' level higher than 'maximum_stock'
return queryset.exclude(maximum_stock=0).filter(
Q(total_in_stock__gt=F('maximum_stock'))
)
# Filter items which have an 'in_stock' level lower than 'maximum_stock'
return queryset.filter(
Q(total_in_stock__lte=F('maximum_stock')) | Q(maximum_stock=0)
).distinct()
# has_stock filter
has_stock = rest_filters.BooleanFilter(label='Has stock', method='filter_has_stock')
@@ -0,0 +1,26 @@
# Generated by Django 5.2.13 on 2026-05-10 06:02
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("part", "0149_bomitem_raw_amount"),
]
operations = [
migrations.AddField(
model_name="part",
name="maximum_stock",
field=models.DecimalField(
decimal_places=6,
default=0,
help_text="Maximum allowed stock level",
max_digits=19,
validators=[django.core.validators.MinValueValidator(0)],
verbose_name="Maximum Stock",
),
),
]
+10
View File
@@ -491,6 +491,7 @@ class Part(
default_location: Where the item is normally stored (may be null)
default_expiry: The default expiry duration for any StockItem instances of this part
minimum_stock: Minimum preferred quantity to keep in stock
maximum_stock: Maximum preferred quantity to keep in stock
units: Units of measure for this part (default='pcs')
salable: Can this part be sold to customers?
assembly: Can this part be build from other parts?
@@ -1232,6 +1233,15 @@ class Part(
help_text=_('Minimum allowed stock level'),
)
maximum_stock = models.DecimalField(
max_digits=19,
decimal_places=6,
default=0,
validators=[MinValueValidator(0)],
verbose_name=_('Maximum Stock'),
help_text=_('Maximum allowed stock level'),
)
units = models.CharField(
max_length=20,
default='',
@@ -600,6 +600,7 @@ class PartSerializer(
'link',
'locked',
'minimum_stock',
'maximum_stock',
'name',
'notes',
'parameters',
@@ -901,6 +902,10 @@ class PartSerializer(
required=False, label=_('Minimum Stock'), default=0
)
maximum_stock = serializers.FloatField(
required=False, label=_('Maximum Stock'), default=0
)
image = InvenTree.serializers.InvenTreeImageSerializerField(
required=False, allow_null=True
)
+52
View File
@@ -2649,6 +2649,58 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
self.assertEqual(data['building'], 55)
self.assertEqual(data['scheduled_to_build'], 32)
def test_low_stock(self):
"""Test the 'low_stock' filter."""
part = Part.objects.create(
name='Low Stock Part',
description='A part which is low on stock',
category=PartCategory.objects.get(pk=1),
minimum_stock=10,
)
response = self.get(
reverse('api-part-list'), {'low_stock': True}, expected_code=200
)
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]['pk'], part.pk)
StockItem.objects.create(part=part, quantity=20)
response = self.get(
reverse('api-part-list'), {'low_stock': True}, expected_code=200
)
# No results should be returned, as the part is no longer low on stock
self.assertEqual(len(response.data), 0)
def test_high_stock(self):
"""Test the 'high_stock' filter."""
part = Part.objects.create(
name='High Stock Part',
description='A part which is high on stock',
category=PartCategory.objects.get(pk=1),
)
StockItem.objects.create(part=part, quantity=100)
response = self.get(
reverse('api-part-list'), {'high_stock': True}, expected_code=200
)
self.assertEqual(len(response.data), 0)
# Set a "maximum stock" threshold for the part
part.maximum_stock = 50
part.save()
response = self.get(
reverse('api-part-list'), {'high_stock': True}, expected_code=200
)
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]['pk'], part.pk)
class BomItemTest(InvenTreeAPITestCase):
"""Unit tests for the BomItem API."""