mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-01 03:00:54 +00:00
Implement structural stock locations (#3949)
* Implement structural stock locations * Bumped API version
This commit is contained in:
@ -309,6 +309,8 @@ class StockLocationList(APIDownloadMixin, ListCreateAPI):
|
||||
]
|
||||
|
||||
filterset_fields = [
|
||||
'name',
|
||||
'structural'
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
|
18
InvenTree/stock/migrations/0090_stocklocation_structural.py
Normal file
18
InvenTree/stock/migrations/0090_stocklocation_structural.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.16 on 2022-11-18 15:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('stock', '0089_alter_stockitem_purchase_price'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='stocklocation',
|
||||
name='structural',
|
||||
field=models.BooleanField(default=False, help_text="Stock items may not be directly located into a structural stock locations, but may be located to it's child locations.", verbose_name='Structural'),
|
||||
),
|
||||
]
|
@ -107,6 +107,14 @@ class StockLocation(InvenTreeBarcodeMixin, MetadataMixin, InvenTreeTree):
|
||||
help_text=_('Select Owner'),
|
||||
related_name='stock_locations')
|
||||
|
||||
structural = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_('Structural'),
|
||||
help_text=_(
|
||||
'Stock items may not be directly located into a structural stock locations, '
|
||||
'but may be located to it\'s child locations.'),
|
||||
)
|
||||
|
||||
def get_location_owner(self):
|
||||
"""Get the closest "owner" for this location.
|
||||
|
||||
@ -139,6 +147,17 @@ class StockLocation(InvenTreeBarcodeMixin, MetadataMixin, InvenTreeTree):
|
||||
|
||||
return user in owner.get_related_owners(include_group=True)
|
||||
|
||||
def clean(self):
|
||||
"""Custom clean action for the StockLocation model:
|
||||
|
||||
- Ensure stock location can't be made structural if stock items already located to them
|
||||
"""
|
||||
if self.pk and self.structural and self.item_count > 0:
|
||||
raise ValidationError(
|
||||
_("You cannot make this stock location structural because some stock items "
|
||||
"are already located into it!"))
|
||||
super().clean()
|
||||
|
||||
def get_absolute_url(self):
|
||||
"""Return url for instance."""
|
||||
return reverse('stock-location-detail', kwargs={'pk': self.id})
|
||||
@ -496,8 +515,14 @@ class StockItem(InvenTreeBarcodeMixin, MetadataMixin, MPTTModel):
|
||||
- The 'part' and 'supplier_part.part' fields cannot point to the same Part object
|
||||
- The 'part' is not virtual
|
||||
- The 'part' does not belong to itself
|
||||
- The location is not structural
|
||||
- Quantity must be 1 if the StockItem has a serial number
|
||||
"""
|
||||
|
||||
if self.location is not None and self.location.structural:
|
||||
raise ValidationError(
|
||||
{'location': _("Stock items cannot be located into structural stock locations!")})
|
||||
|
||||
super().clean()
|
||||
|
||||
# Strip serial number field
|
||||
|
@ -606,6 +606,7 @@ class LocationSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
'items',
|
||||
'owner',
|
||||
'icon',
|
||||
'structural',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
|
@ -6,6 +6,7 @@ from datetime import datetime, timedelta
|
||||
from enum import IntEnum
|
||||
|
||||
import django.http
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.urls import reverse
|
||||
|
||||
import tablib
|
||||
@ -225,6 +226,71 @@ class StockLocationTest(StockAPITestCase):
|
||||
child.refresh_from_db()
|
||||
self.assertEqual(child.parent, parent_stock_location)
|
||||
|
||||
def test_stock_location_structural(self):
|
||||
"""Test the effectiveness of structural stock locations
|
||||
|
||||
Make sure:
|
||||
- Stock items cannot be created in structural locations
|
||||
- Stock items cannot be located to structural locations
|
||||
- Check that stock location change to structural fails if items located into it
|
||||
"""
|
||||
|
||||
# Create our structural stock location
|
||||
structural_location = StockLocation.objects.create(
|
||||
name='Structural stock location',
|
||||
description='This is the structural stock location',
|
||||
parent=None,
|
||||
structural=True
|
||||
)
|
||||
|
||||
stock_item_count_before = StockItem.objects.count()
|
||||
|
||||
# Make sure that we get an error if we try to create a stock item in the structural location
|
||||
with self.assertRaises(ValidationError):
|
||||
item = StockItem.objects.create(
|
||||
batch="Stock item which shall not be created",
|
||||
location=structural_location
|
||||
)
|
||||
|
||||
# Ensure that the stock item really did not get created in the structural location
|
||||
self.assertEqual(stock_item_count_before, StockItem.objects.count())
|
||||
|
||||
# Create a non-structural location for test stock location change
|
||||
non_structural_location = StockLocation.objects.create(
|
||||
name='Non-structural category',
|
||||
description='This is a non-structural category',
|
||||
parent=None,
|
||||
structural=False
|
||||
)
|
||||
|
||||
# Construct a part for stock item creation
|
||||
part = Part.objects.create(
|
||||
name='Part for stock item creation', description='Part for stock item creation',
|
||||
category=None,
|
||||
is_template=False,
|
||||
)
|
||||
|
||||
# Create the test stock item located to a non-structural category
|
||||
item = StockItem.objects.create(
|
||||
batch="Item which will be tried to relocated to a structural location",
|
||||
location=non_structural_location,
|
||||
part=part
|
||||
)
|
||||
|
||||
# Try to relocate it to a structural location
|
||||
item.location = structural_location
|
||||
with self.assertRaises(ValidationError):
|
||||
item.save()
|
||||
|
||||
# Ensure that the item did not get saved to the DB
|
||||
item.refresh_from_db()
|
||||
self.assertEqual(item.location.pk, non_structural_location.pk)
|
||||
|
||||
# Try to change the non-structural location to structural while items located into it
|
||||
non_structural_location.structural = True
|
||||
with self.assertRaises(ValidationError):
|
||||
non_structural_location.full_clean()
|
||||
|
||||
|
||||
class StockItemListTest(StockAPITestCase):
|
||||
"""Tests for the StockItem API LIST endpoint."""
|
||||
|
Reference in New Issue
Block a user