mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Add some unit tests for the new endpoint
This commit is contained in:
parent
943b27e195
commit
31dbb9563b
@ -252,6 +252,9 @@ class StockHistoryCode(StatusCode):
|
||||
SPLIT_FROM_PARENT = 40
|
||||
SPLIT_CHILD_ITEM = 42
|
||||
|
||||
# Stock merging operations
|
||||
MERGED_STOCK_ITEMS = 45
|
||||
|
||||
# Build order codes
|
||||
BUILD_OUTPUT_CREATED = 50
|
||||
BUILD_OUTPUT_COMPLETED = 55
|
||||
@ -288,6 +291,8 @@ class StockHistoryCode(StatusCode):
|
||||
SPLIT_FROM_PARENT: _('Split from parent item'),
|
||||
SPLIT_CHILD_ITEM: _('Split child item'),
|
||||
|
||||
MERGED_STOCK_ITEMS: _('Merged stock items'),
|
||||
|
||||
SENT_TO_CUSTOMER: _('Sent to customer'),
|
||||
RETURNED_FROM_CUSTOMER: _('Returned from customer'),
|
||||
|
||||
|
@ -114,19 +114,6 @@
|
||||
lft: 0
|
||||
rght: 0
|
||||
|
||||
- model: stock.stockitem
|
||||
pk: 501
|
||||
fields:
|
||||
part: 10001
|
||||
location: 7
|
||||
batch: "AAA"
|
||||
quantity: 1
|
||||
serial: 1
|
||||
level: 0
|
||||
tree_id: 0
|
||||
lft: 0
|
||||
rght: 0
|
||||
|
||||
- model: stock.stockitem
|
||||
pk: 501
|
||||
fields:
|
||||
|
@ -1153,18 +1153,51 @@ class StockItem(MPTTModel):
|
||||
result.stock_item = self
|
||||
result.save()
|
||||
|
||||
def can_merge(self, other=None, raise_error=False):
|
||||
def can_merge(self, other=None, raise_error=False, **kwargs):
|
||||
"""
|
||||
Check if this stock item can be merged into another
|
||||
Check if this stock item can be merged into another stock item
|
||||
"""
|
||||
|
||||
allow_mismatched_suppliers = kwargs.get('allow_mismatched_suppliers', False)
|
||||
|
||||
allow_mismatched_status = kwargs.get('allow_mismatched_status', False)
|
||||
|
||||
try:
|
||||
if not self.in_stock:
|
||||
raise ValidationError(_("Item must be in stock"))
|
||||
# Generic checks (do not rely on the 'other' part)
|
||||
if self.sales_order:
|
||||
raise ValidationError(_('Stock item has been assigned to a sales order'))
|
||||
|
||||
if self.belongs_to:
|
||||
raise ValidationError(_('Stock item is installed in another item'))
|
||||
|
||||
if self.customer:
|
||||
raise ValidationError(_('Stock item has been assigned to a customer'))
|
||||
|
||||
if self.is_building:
|
||||
raise ValidationError(_('Stock item is currently in production'))
|
||||
|
||||
if self.serialized:
|
||||
raise ValidationError(_("Serialized stock cannot be merged"))
|
||||
|
||||
if other:
|
||||
# Specific checks (rely on the 'other' part)
|
||||
|
||||
# Prevent stock item being merged with itself
|
||||
if self == other:
|
||||
raise ValidationError(_('Duplicate stock items'))
|
||||
|
||||
# Base part must match
|
||||
if self.part != other.part:
|
||||
raise ValidationError(_("Stock items must refer to the same part"))
|
||||
|
||||
# Check if supplier part references match
|
||||
if self.supplier_part != other.supplier_part and not allow_mismatched_suppliers:
|
||||
raise ValidationError(_("Stock items must refer to the same supplier part"))
|
||||
|
||||
# Check if stock status codes match
|
||||
if self.status != other.status and not allow_mismatched_status:
|
||||
raise ValidationError(_("Stock status codes must match"))
|
||||
|
||||
except ValidationError as e:
|
||||
if raise_error:
|
||||
raise e
|
||||
@ -1174,7 +1207,7 @@ class StockItem(MPTTModel):
|
||||
return True
|
||||
|
||||
@transaction.atomic
|
||||
def merge_stock_item(self, other, **kwargs):
|
||||
def merge_stock_items(self, other_items, raise_error=False, **kwargs):
|
||||
"""
|
||||
Merge another stock item into this one; the two become one!
|
||||
|
||||
@ -1185,15 +1218,48 @@ class StockItem(MPTTModel):
|
||||
- Any allocations (build order, sales order) are moved to this StockItem
|
||||
"""
|
||||
|
||||
# If the stock item cannot be merged, return
|
||||
if not self.can_merge(other):
|
||||
if len(other_items) == 0:
|
||||
return
|
||||
|
||||
user = kwargs.get('user', None)
|
||||
location = kwargs.get('location', None)
|
||||
notes = kwargs.get('notes', None)
|
||||
|
||||
# TODO: Merge!
|
||||
for other in other_items:
|
||||
# If the stock item cannot be merged, return
|
||||
if not self.can_merge(other, raise_error=raise_error, **kwargs):
|
||||
return
|
||||
|
||||
for other in other_items:
|
||||
|
||||
self.quantity += other.quantity
|
||||
|
||||
# Any "build order allocations" for the other item must be assigned to this one
|
||||
for allocation in other.allocations.all():
|
||||
|
||||
allocation.stock_item = self
|
||||
allocation.save()
|
||||
|
||||
# Any "sales order allocations" for the other item must be assigned to this one
|
||||
for allocation in other.sales_order_allocations.all():
|
||||
|
||||
allocation.stock_item = self()
|
||||
allocation.save()
|
||||
|
||||
# Delete the other stock item
|
||||
other.delete()
|
||||
|
||||
self.add_tracking_entry(
|
||||
StockHistoryCode.MERGED_STOCK_ITEMS,
|
||||
user,
|
||||
notes=notes,
|
||||
deltas={
|
||||
'location': location.pk,
|
||||
}
|
||||
)
|
||||
|
||||
self.location = location
|
||||
self.save()
|
||||
|
||||
@transaction.atomic
|
||||
def splitStock(self, quantity, location, user, **kwargs):
|
||||
|
@ -701,6 +701,7 @@ class StockMergeItemSerializer(serializers.Serializer):
|
||||
|
||||
return item
|
||||
|
||||
|
||||
class StockMergeSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for merging two (or more) stock items together
|
||||
@ -710,6 +711,9 @@ class StockMergeSerializer(serializers.Serializer):
|
||||
fields = [
|
||||
'items',
|
||||
'location',
|
||||
'notes',
|
||||
'allow_mismatched_suppliers',
|
||||
'allow_mismatched_status',
|
||||
]
|
||||
|
||||
items = StockMergeItemSerializer(
|
||||
@ -726,6 +730,25 @@ class StockMergeSerializer(serializers.Serializer):
|
||||
help_text=_('Destination stock location'),
|
||||
)
|
||||
|
||||
notes = serializers.CharField(
|
||||
required=False,
|
||||
allow_blank=True,
|
||||
label=_('Notes'),
|
||||
help_text=_('Stock merging notes'),
|
||||
)
|
||||
|
||||
allow_mismatched_suppliers = serializers.BooleanField(
|
||||
required=False,
|
||||
label=_('Allow mismatched suppliers'),
|
||||
help_text=_('Allow stock items with different supplier parts to be merged'),
|
||||
)
|
||||
|
||||
allow_mismatched_status = serializers.BooleanField(
|
||||
required=False,
|
||||
label=_('Allow mismatched status'),
|
||||
help_text=_('Allow stock items with different status codes to be merged'),
|
||||
)
|
||||
|
||||
def validate(self, data):
|
||||
|
||||
data = super().validate(data)
|
||||
@ -735,11 +758,63 @@ class StockMergeSerializer(serializers.Serializer):
|
||||
if len(items) < 2:
|
||||
raise ValidationError(_('At least two stock items must be provided'))
|
||||
|
||||
unique_items = set()
|
||||
|
||||
# The "base item" is the first item
|
||||
base_item = items[0]['item']
|
||||
|
||||
data['base_item'] = base_item
|
||||
|
||||
# Ensure stock items are unique!
|
||||
for element in items:
|
||||
item = element['item']
|
||||
|
||||
if item.pk in unique_items:
|
||||
raise ValidationError(_('Duplicate stock items'))
|
||||
|
||||
unique_items.add(item.pk)
|
||||
|
||||
# Checks from here refer to the "base_item"
|
||||
if item == base_item:
|
||||
continue
|
||||
|
||||
# Check that this item can be merged with the base_item
|
||||
item.can_merge(
|
||||
raise_error=True,
|
||||
other=base_item,
|
||||
allow_mismatched_suppliers=data.get('allow_mismatched_suppliers', False),
|
||||
allow_mismatched_status=data.get('allow_mismatched_status', False),
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
def save(self):
|
||||
# TODO
|
||||
pass
|
||||
"""
|
||||
Actually perform the stock merging action.
|
||||
At this point we are confident that the merge can take place
|
||||
"""
|
||||
|
||||
data = self.validated_data
|
||||
|
||||
base_item = data['base_item']
|
||||
items = data['items'][1:]
|
||||
|
||||
request = self.context['request']
|
||||
user = getattr(request, 'user', None)
|
||||
|
||||
items = []
|
||||
|
||||
for item in data['items'][1:]:
|
||||
items.append(item['item'])
|
||||
|
||||
base_item.merge_stock_items(
|
||||
items,
|
||||
allow_mismatched_suppliers=data.get('allow_mismatched_suppliers', False),
|
||||
allow_mismatched_status=data.get('allow_mismatched_status', False),
|
||||
user=user,
|
||||
location=data['location'],
|
||||
notes=data.get('notes', None)
|
||||
)
|
||||
|
||||
|
||||
class StockAdjustmentItemSerializer(serializers.Serializer):
|
||||
|
@ -842,3 +842,189 @@ class StockAssignTest(StockAPITestCase):
|
||||
|
||||
# 5 stock items should now have been assigned to this customer
|
||||
self.assertEqual(customer.assigned_stock.count(), 5)
|
||||
|
||||
|
||||
class StockMergeTest(StockAPITestCase):
|
||||
"""
|
||||
Unit tests for merging stock items via the API
|
||||
"""
|
||||
|
||||
URL = reverse('api-stock-merge')
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super().setUp()
|
||||
|
||||
self.part = part.models.Part.objects.get(pk=25)
|
||||
self.loc = StockLocation.objects.get(pk=1)
|
||||
self.sp_1 = company.models.SupplierPart.objects.get(pk=100)
|
||||
self.sp_2 = company.models.SupplierPart.objects.get(pk=101)
|
||||
|
||||
self.item_1 = StockItem.objects.create(
|
||||
part=self.part,
|
||||
supplier_part=self.sp_1,
|
||||
quantity=100,
|
||||
)
|
||||
|
||||
self.item_2 = StockItem.objects.create(
|
||||
part=self.part,
|
||||
supplier_part=self.sp_2,
|
||||
quantity=100,
|
||||
)
|
||||
|
||||
self.item_3 = StockItem.objects.create(
|
||||
part=self.part,
|
||||
supplier_part=self.sp_2,
|
||||
quantity=50,
|
||||
)
|
||||
|
||||
def test_missing_data(self):
|
||||
"""
|
||||
Test responses which are missing required data
|
||||
"""
|
||||
|
||||
# Post completely empty
|
||||
|
||||
data = self.post(
|
||||
self.URL,
|
||||
{},
|
||||
expected_code=400
|
||||
).data
|
||||
|
||||
self.assertIn('This field is required', str(data['items']))
|
||||
self.assertIn('This field is required', str(data['location']))
|
||||
|
||||
# Post with a location and empty items list
|
||||
data = self.post(
|
||||
self.URL,
|
||||
{
|
||||
'items': [],
|
||||
'location': 1,
|
||||
},
|
||||
expected_code=400
|
||||
).data
|
||||
|
||||
self.assertIn('At least two stock items', str(data))
|
||||
|
||||
def test_invalid_data(self):
|
||||
"""
|
||||
Test responses which have invalid data
|
||||
"""
|
||||
|
||||
# Serialized stock items should be rejected
|
||||
data = self.post(
|
||||
self.URL,
|
||||
{
|
||||
'items': [
|
||||
{
|
||||
'item': 501,
|
||||
},
|
||||
{
|
||||
'item': 502,
|
||||
}
|
||||
],
|
||||
'location': 1,
|
||||
},
|
||||
expected_code=400,
|
||||
).data
|
||||
|
||||
self.assertIn('Serialized stock cannot be merged', str(data))
|
||||
|
||||
# Prevent item duplication
|
||||
|
||||
data = self.post(
|
||||
self.URL,
|
||||
{
|
||||
'items': [
|
||||
{
|
||||
'item': 11,
|
||||
},
|
||||
{
|
||||
'item': 11,
|
||||
}
|
||||
],
|
||||
'location': 1,
|
||||
},
|
||||
expected_code=400,
|
||||
).data
|
||||
|
||||
self.assertIn('Duplicate stock items', str(data))
|
||||
|
||||
# Check for mismatching stock items
|
||||
data = self.post(
|
||||
self.URL,
|
||||
{
|
||||
'items': [
|
||||
{
|
||||
'item': 1234,
|
||||
},
|
||||
{
|
||||
'item': 11,
|
||||
}
|
||||
],
|
||||
'location': 1,
|
||||
},
|
||||
expected_code=400,
|
||||
).data
|
||||
|
||||
self.assertIn('Stock items must refer to the same part', str(data))
|
||||
|
||||
# Check for mismatching supplier parts
|
||||
payload = {
|
||||
'items': [
|
||||
{
|
||||
'item': self.item_1.pk,
|
||||
},
|
||||
{
|
||||
'item': self.item_2.pk,
|
||||
},
|
||||
],
|
||||
'location': 1,
|
||||
}
|
||||
|
||||
data = self.post(
|
||||
self.URL,
|
||||
payload,
|
||||
expected_code=400,
|
||||
).data
|
||||
|
||||
self.assertIn('Stock items must refer to the same supplier part', str(data))
|
||||
|
||||
def test_valid_merge(self):
|
||||
"""
|
||||
Test valid merging of stock items
|
||||
"""
|
||||
|
||||
# Check initial conditions
|
||||
n = StockItem.objects.filter(part=self.part).count()
|
||||
self.assertEqual(self.item_1.quantity, 100)
|
||||
|
||||
payload = {
|
||||
'items': [
|
||||
{
|
||||
'item': self.item_1.pk,
|
||||
},
|
||||
{
|
||||
'item': self.item_2.pk,
|
||||
},
|
||||
{
|
||||
'item': self.item_3.pk,
|
||||
},
|
||||
],
|
||||
'location': 1,
|
||||
'allow_mismatched_suppliers': True,
|
||||
}
|
||||
|
||||
self.post(
|
||||
self.URL,
|
||||
payload,
|
||||
expected_code=201,
|
||||
)
|
||||
|
||||
self.item_1.refresh_from_db()
|
||||
|
||||
# Stock quantity should have been increased!
|
||||
self.assertEqual(self.item_1.quantity, 250)
|
||||
|
||||
# Total number of stock items has been reduced!
|
||||
self.assertEqual(StockItem.objects.filter(part=self.part).count(), n - 2)
|
||||
|
Loading…
Reference in New Issue
Block a user