mirror of
https://github.com/inventree/InvenTree.git
synced 2026-11-10 10:57:52 +00:00
* feat(stock): add exclude_from_stock option to StockLocation (#7064) * feat(stock): add UI detail, table filters, API version bump, and prek formatting (#7064) --------- Co-authored-by: UGREEN USER <jaharoni@debian.dedomain> Co-authored-by: WholeTroutMedia <cortex@inceptionengine.systems> Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
This commit is contained in:
12 files changed
+105
-3
No files matched your search
@@ -1,11 +1,15 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 570
|
||||
INVENTREE_API_VERSION = 571
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v571 -> 2026-11-03 : https://github.com/inventree/InvenTree/pull/13166
|
||||
- Adds "exclude_from_stock" field and filter to the StockLocation API endpoint
|
||||
- Adds "exclude_from_stock" filter to the StockItem API endpoint
|
||||
|
||||
v570 -> 2026-11-03 : https://github.com/inventree/InvenTree/pull/13175
|
||||
- Token API (/api/user/tokens/): "revoked", "revoked_by", "issued_by", "token_version", "revocation_reason" and "last_seen" fields are read-only
|
||||
- Token API (/api/user/tokens/): "expiry" must be between today and 365 days in the future
|
||||
|
||||
@@ -310,7 +310,7 @@ class StockLocationFilter(FilterSet):
|
||||
"""Meta class options for this filterset."""
|
||||
|
||||
model = StockLocation
|
||||
fields = ['name', 'structural', 'external']
|
||||
fields = ['name', 'structural', 'external', 'exclude_from_stock']
|
||||
|
||||
location_type = rest_filters.ModelChoiceFilter(
|
||||
queryset=StockLocationType.objects.all(), field_name='location_type'
|
||||
@@ -765,6 +765,16 @@ class StockFilter(FilterSet):
|
||||
return queryset.filter(location__external=True)
|
||||
return queryset.exclude(location__external=True)
|
||||
|
||||
exclude_from_stock = rest_filters.BooleanFilter(
|
||||
label=_('Exclude from Stock'), method='filter_exclude_from_stock'
|
||||
)
|
||||
|
||||
def filter_exclude_from_stock(self, queryset, name, value):
|
||||
"""Filter by whether or not the stock item is located in an exclude_from_stock location."""
|
||||
if str2bool(value):
|
||||
return queryset.filter(location__exclude_from_stock=True)
|
||||
return queryset.exclude(location__exclude_from_stock=True)
|
||||
|
||||
in_stock = rest_filters.BooleanFilter(label='In Stock', method='filter_in_stock')
|
||||
|
||||
def filter_in_stock(self, queryset, name, value):
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.2.17 on 2026-11-02 21:10
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [('stock', '0130_delete_stockitemtracking')]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='stocklocation',
|
||||
name='exclude_from_stock',
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text='If checked, stock items in this location will not be counted in available stock',
|
||||
verbose_name='Exclude from Stock',
|
||||
),
|
||||
)
|
||||
]
|
||||
@@ -213,6 +213,14 @@ class StockLocation(
|
||||
help_text=_('This is an external stock location'),
|
||||
)
|
||||
|
||||
exclude_from_stock = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_('Exclude from Stock'),
|
||||
help_text=_(
|
||||
'If checked, stock items in this location will not be counted in available stock'
|
||||
),
|
||||
)
|
||||
|
||||
location_type = models.ForeignKey(
|
||||
StockLocationType,
|
||||
on_delete=models.SET_NULL,
|
||||
@@ -658,7 +666,7 @@ class StockItem(
|
||||
consumed_by=None,
|
||||
is_building=False,
|
||||
status__in=StockStatusGroups.AVAILABLE_CODES,
|
||||
)
|
||||
) & (Q(location=None) | Q(location__exclude_from_stock=False))
|
||||
|
||||
@classmethod
|
||||
def get_expired_filter(cls):
|
||||
@@ -2318,6 +2326,9 @@ class StockItem(
|
||||
if check_status and self.status not in StockStatusGroups.AVAILABLE_CODES:
|
||||
return False
|
||||
|
||||
if check_status and self.location and self.location.exclude_from_stock:
|
||||
return False
|
||||
|
||||
if check_quantity and self.quantity <= 0:
|
||||
return False
|
||||
|
||||
|
||||
@@ -1428,6 +1428,7 @@ class LocationSerializer(
|
||||
'custom_icon',
|
||||
'structural',
|
||||
'external',
|
||||
'exclude_from_stock',
|
||||
'location_type',
|
||||
# Optional fields
|
||||
'location_type_detail',
|
||||
|
||||
@@ -981,6 +981,14 @@ class StockItemListTest(StockAPITestCase):
|
||||
response = self.get_stock(external=False)
|
||||
self.assertEqual(len(response), 28)
|
||||
|
||||
def test_filter_exclude_from_stock(self):
|
||||
"""Filter StockItem by exclude_from_stock."""
|
||||
response = self.get_stock(exclude_from_stock=True)
|
||||
self.assertEqual(len(response), 0)
|
||||
|
||||
response = self.get_stock(exclude_from_stock=False)
|
||||
self.assertEqual(len(response), 29)
|
||||
|
||||
def test_filter_available(self):
|
||||
"""Filter StockItem by available."""
|
||||
response = self.get_stock(available=True)
|
||||
|
||||
@@ -1930,6 +1930,33 @@ class StockLocationTest(InvenTreeTestCase):
|
||||
loc.save()
|
||||
self.assertEqual(loc.icon, '')
|
||||
|
||||
def test_exclude_from_stock(self):
|
||||
"""Test that stock items in locations with exclude_from_stock=True are excluded from available stock."""
|
||||
part = Part.objects.create(
|
||||
name='RnD Test Part', description='Part for RnD stock test'
|
||||
)
|
||||
normal_loc = StockLocation.objects.create(
|
||||
name='Warehouse A', exclude_from_stock=False
|
||||
)
|
||||
rnd_loc = StockLocation.objects.create(name='RnD Lab', exclude_from_stock=True)
|
||||
|
||||
item_normal = StockItem.objects.create(
|
||||
part=part, location=normal_loc, quantity=10
|
||||
)
|
||||
item_rnd = StockItem.objects.create(part=part, location=rnd_loc, quantity=25)
|
||||
|
||||
self.assertTrue(item_normal.in_stock)
|
||||
self.assertFalse(item_rnd.in_stock)
|
||||
self.assertEqual(part.total_stock, 10)
|
||||
self.assertEqual(part.available_stock, 10)
|
||||
|
||||
rnd_loc.exclude_from_stock = False
|
||||
rnd_loc.save()
|
||||
item_rnd.refresh_from_db()
|
||||
self.assertTrue(item_rnd.in_stock)
|
||||
self.assertEqual(part.total_stock, 35)
|
||||
self.assertEqual(part.available_stock, 35)
|
||||
|
||||
|
||||
class AdminTest(AdminTestCase):
|
||||
"""Tests for the admin interface integration."""
|
||||
|
||||
@@ -2195,6 +2195,7 @@ export function useStockLocationFields(): ApiFormFieldSet {
|
||||
},
|
||||
structural: {},
|
||||
external: {},
|
||||
exclude_from_stock: {},
|
||||
custom_icon: {
|
||||
field_type: 'icon'
|
||||
},
|
||||
|
||||
@@ -78,6 +78,11 @@ export function StockLocationDetailsPanel({
|
||||
name: 'external',
|
||||
label: t`External`
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'exclude_from_stock',
|
||||
label: t`Exclude from Stock`
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'location_type_detail.name',
|
||||
|
||||
@@ -320,6 +320,11 @@ function stockItemTableFilters({
|
||||
label: t`External Location`,
|
||||
description: t`Show items in an external location`
|
||||
},
|
||||
{
|
||||
name: 'exclude_from_stock',
|
||||
label: t`Exclude from Stock`,
|
||||
description: t`Show items in a location excluded from stock calculations`
|
||||
},
|
||||
TagsFilter({ modelType: ModelType.stockitem })
|
||||
];
|
||||
}
|
||||
|
||||
@@ -51,6 +51,11 @@ export function StockLocationTable({ parentId }: Readonly<{ parentId?: any }>) {
|
||||
label: t`External`,
|
||||
description: t`Show external locations`
|
||||
},
|
||||
{
|
||||
name: 'exclude_from_stock',
|
||||
label: t`Exclude from Stock`,
|
||||
description: t`Show locations excluded from stock calculations`
|
||||
},
|
||||
{
|
||||
name: 'has_location_type',
|
||||
label: t`Has location type`
|
||||
@@ -97,6 +102,10 @@ export function StockLocationTable({ parentId }: Readonly<{ parentId?: any }>) {
|
||||
accessor: 'external',
|
||||
defaultVisible: false
|
||||
}),
|
||||
BooleanColumn({
|
||||
accessor: 'exclude_from_stock',
|
||||
defaultVisible: false
|
||||
}),
|
||||
{
|
||||
accessor: 'location_type',
|
||||
sortable: false,
|
||||
|
||||
Reference in new issue
Block a user