mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'inventree:master' into matmair/issue2694
This commit is contained in:
commit
ec021624cd
@ -133,7 +133,7 @@ class ReferenceIndexingMixin(models.Model):
|
|||||||
reference_int = models.BigIntegerField(default=0)
|
reference_int = models.BigIntegerField(default=0)
|
||||||
|
|
||||||
|
|
||||||
def extract_int(reference):
|
def extract_int(reference, clip=0x7fffffff):
|
||||||
# Default value if we cannot convert to an integer
|
# Default value if we cannot convert to an integer
|
||||||
ref_int = 0
|
ref_int = 0
|
||||||
|
|
||||||
@ -146,6 +146,15 @@ def extract_int(reference):
|
|||||||
ref_int = int(ref)
|
ref_int = int(ref)
|
||||||
except:
|
except:
|
||||||
ref_int = 0
|
ref_int = 0
|
||||||
|
|
||||||
|
# Ensure that the returned values are within the range that can be stored in an IntegerField
|
||||||
|
# Note: This will result in large values being "clipped"
|
||||||
|
if clip is not None:
|
||||||
|
if ref_int > clip:
|
||||||
|
ref_int = clip
|
||||||
|
elif ref_int < -clip:
|
||||||
|
ref_int = -clip
|
||||||
|
|
||||||
return ref_int
|
return ref_int
|
||||||
|
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ from rest_framework.views import APIView
|
|||||||
from stock.models import StockItem
|
from stock.models import StockItem
|
||||||
from stock.serializers import StockItemSerializer
|
from stock.serializers import StockItemSerializer
|
||||||
|
|
||||||
|
from barcodes.plugins.inventree_barcode import InvenTreeBarcodePlugin
|
||||||
from barcodes.barcode import hash_barcode
|
from barcodes.barcode import hash_barcode
|
||||||
from plugin import registry
|
from plugin import registry
|
||||||
|
|
||||||
@ -57,6 +58,9 @@ class BarcodeScan(APIView):
|
|||||||
|
|
||||||
barcode_data = data.get('barcode')
|
barcode_data = data.get('barcode')
|
||||||
|
|
||||||
|
# Ensure that the default barcode handler is installed
|
||||||
|
plugins.append(InvenTreeBarcodePlugin())
|
||||||
|
|
||||||
# Look for a barcode plugin which knows how to deal with this barcode
|
# Look for a barcode plugin which knows how to deal with this barcode
|
||||||
plugin = None
|
plugin = None
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ class InvenTreeBarcodePlugin(BarcodePlugin):
|
|||||||
# If any of the following keys are in the JSON data,
|
# If any of the following keys are in the JSON data,
|
||||||
# let's go ahead and assume that the code is a valid InvenTree one...
|
# let's go ahead and assume that the code is a valid InvenTree one...
|
||||||
|
|
||||||
for key in ['tool', 'version', 'InvenTree', 'stockitem', 'location', 'part']:
|
for key in ['tool', 'version', 'InvenTree', 'stockitem', 'stocklocation', 'part']:
|
||||||
if key in self.data.keys():
|
if key in self.data.keys():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -56,6 +56,66 @@ class BarcodeAPITest(APITestCase):
|
|||||||
self.assertIn('plugin', data)
|
self.assertIn('plugin', data)
|
||||||
self.assertIsNone(data['plugin'])
|
self.assertIsNone(data['plugin'])
|
||||||
|
|
||||||
|
def test_find_part(self):
|
||||||
|
"""
|
||||||
|
Test that we can lookup a part based on ID
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
self.scan_url,
|
||||||
|
{
|
||||||
|
'barcode': {
|
||||||
|
'part': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
format='json',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertIn('part', response.data)
|
||||||
|
self.assertIn('barcode_data', response.data)
|
||||||
|
self.assertEqual(response.data['part']['pk'], 1)
|
||||||
|
|
||||||
|
def test_find_stock_item(self):
|
||||||
|
"""
|
||||||
|
Test that we can lookup a stock item based on ID
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
self.scan_url,
|
||||||
|
{
|
||||||
|
'barcode': {
|
||||||
|
'stockitem': 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
format='json',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertIn('stockitem', response.data)
|
||||||
|
self.assertIn('barcode_data', response.data)
|
||||||
|
self.assertEqual(response.data['stockitem']['pk'], 1)
|
||||||
|
|
||||||
|
def test_find_location(self):
|
||||||
|
"""
|
||||||
|
Test that we can lookup a stock location based on ID
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
self.scan_url,
|
||||||
|
{
|
||||||
|
'barcode': {
|
||||||
|
'stocklocation': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertIn('stocklocation', response.data)
|
||||||
|
self.assertIn('barcode_data', response.data)
|
||||||
|
self.assertEqual(response.data['stocklocation']['pk'], 1)
|
||||||
|
|
||||||
def test_integer_barcode(self):
|
def test_integer_barcode(self):
|
||||||
|
|
||||||
response = self.postBarcode(self.scan_url, '123456789')
|
response = self.postBarcode(self.scan_url, '123456789')
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -112,17 +112,16 @@ class PurchaseOrderTest(OrderTest):
|
|||||||
self.assignRole('purchase_order.add')
|
self.assignRole('purchase_order.add')
|
||||||
|
|
||||||
url = reverse('api-po-list')
|
url = reverse('api-po-list')
|
||||||
huge_numer = 9223372036854775808
|
huge_number = 9223372036854775808
|
||||||
|
|
||||||
# too big
|
|
||||||
self.post(
|
self.post(
|
||||||
url,
|
url,
|
||||||
{
|
{
|
||||||
'supplier': 1,
|
'supplier': 1,
|
||||||
'reference': huge_numer,
|
'reference': huge_number,
|
||||||
'description': 'PO not created via the API',
|
'description': 'PO not created via the API',
|
||||||
},
|
},
|
||||||
expected_code=400
|
expected_code=201,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_po_attachments(self):
|
def test_po_attachments(self):
|
||||||
|
@ -5,7 +5,7 @@ This module provides template tags for extra functionality,
|
|||||||
over and above the built-in Django tags.
|
over and above the built-in Django tags.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import date
|
from datetime import date, datetime
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@ -87,7 +87,9 @@ def render_date(context, date_object):
|
|||||||
# Update the context cache
|
# Update the context cache
|
||||||
context['user_date_format'] = user_date_format
|
context['user_date_format'] = user_date_format
|
||||||
|
|
||||||
return date_object.strftime(user_date_format)
|
if isinstance(date_object, (datetime, date)):
|
||||||
|
return date_object.strftime(user_date_format)
|
||||||
|
return date_object
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
|
@ -269,10 +269,62 @@ class StockItem(MPTTModel):
|
|||||||
serial_int = 0
|
serial_int = 0
|
||||||
|
|
||||||
if serial is not None:
|
if serial is not None:
|
||||||
serial_int = extract_int(str(serial))
|
|
||||||
|
serial = str(serial).strip()
|
||||||
|
|
||||||
|
serial_int = extract_int(serial)
|
||||||
|
|
||||||
self.serial_int = serial_int
|
self.serial_int = serial_int
|
||||||
|
|
||||||
|
def get_next_serialized_item(self, include_variants=True, reverse=False):
|
||||||
|
"""
|
||||||
|
Get the "next" serial number for the part this stock item references.
|
||||||
|
|
||||||
|
e.g. if this stock item has a serial number 100, we may return the stock item with serial number 101
|
||||||
|
|
||||||
|
Note that this only works for "serialized" stock items with integer values
|
||||||
|
|
||||||
|
Args:
|
||||||
|
include_variants: True if we wish to include stock for variant parts
|
||||||
|
reverse: True if we want to return the "previous" (lower) serial number
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A StockItem object matching the requirements, or None
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self.serialized:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Find only serialized stock items
|
||||||
|
items = StockItem.objects.exclude(serial=None).exclude(serial='')
|
||||||
|
|
||||||
|
if include_variants:
|
||||||
|
# Match against any part within the variant tree
|
||||||
|
items = items.filter(part__tree_id=self.part.tree_id)
|
||||||
|
else:
|
||||||
|
# Match only against the specific part
|
||||||
|
items = items.filter(part=self.part)
|
||||||
|
|
||||||
|
serial = self.serial_int
|
||||||
|
|
||||||
|
if reverse:
|
||||||
|
# Select only stock items with lower serial numbers, in decreasing order
|
||||||
|
items = items.filter(serial_int__lt=serial)
|
||||||
|
items = items.order_by('-serial_int')
|
||||||
|
else:
|
||||||
|
# Select only stock items with higher serial numbers, in increasing order
|
||||||
|
items = items.filter(serial_int__gt=serial)
|
||||||
|
items = items.order_by('serial_int')
|
||||||
|
|
||||||
|
if items.count() > 0:
|
||||||
|
item = items.first()
|
||||||
|
|
||||||
|
if item.serialized:
|
||||||
|
return item
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Save this StockItem to the database. Performs a number of checks:
|
Save this StockItem to the database. Performs a number of checks:
|
||||||
|
@ -380,6 +380,84 @@ class StockTest(TestCase):
|
|||||||
item.save()
|
item.save()
|
||||||
self.assertTrue(item.serialized)
|
self.assertTrue(item.serialized)
|
||||||
|
|
||||||
|
def test_big_serials(self):
|
||||||
|
"""
|
||||||
|
Unit tests for "large" serial numbers which exceed integer encoding
|
||||||
|
"""
|
||||||
|
|
||||||
|
p = Part.objects.create(
|
||||||
|
name='trackable part',
|
||||||
|
description='trackable part',
|
||||||
|
trackable=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
item = StockItem.objects.create(
|
||||||
|
part=p,
|
||||||
|
quantity=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
for sn in [12345, '12345', ' 12345 ']:
|
||||||
|
item.serial = sn
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
self.assertEqual(item.serial_int, 12345)
|
||||||
|
|
||||||
|
item.serial = "-123"
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
# Negative number should map to zero
|
||||||
|
self.assertEqual(item.serial_int, 0)
|
||||||
|
|
||||||
|
# Test a very very large value
|
||||||
|
item.serial = '99999999999999999999999999999999999999999999999999999'
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
self.assertEqual(item.serial_int, 0x7fffffff)
|
||||||
|
|
||||||
|
# Non-numeric values should encode to zero
|
||||||
|
for sn in ['apple', 'banana', 'carrot']:
|
||||||
|
item.serial = sn
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
self.assertEqual(item.serial_int, 0)
|
||||||
|
|
||||||
|
# Next, test for incremenet / decrement functionality
|
||||||
|
item.serial = 100
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
item_next = StockItem.objects.create(
|
||||||
|
part=p,
|
||||||
|
serial=150,
|
||||||
|
quantity=1
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(item.get_next_serialized_item(), item_next)
|
||||||
|
|
||||||
|
item_prev = StockItem.objects.create(
|
||||||
|
part=p,
|
||||||
|
serial=' 57',
|
||||||
|
quantity=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(item.get_next_serialized_item(reverse=True), item_prev)
|
||||||
|
|
||||||
|
# Create a number of serialized stock items around the current item
|
||||||
|
for i in range(75, 125):
|
||||||
|
try:
|
||||||
|
StockItem.objects.create(
|
||||||
|
part=p,
|
||||||
|
serial=i,
|
||||||
|
quantity=1,
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
item_next = item.get_next_serialized_item()
|
||||||
|
item_prev = item.get_next_serialized_item(reverse=True)
|
||||||
|
|
||||||
|
self.assertEqual(item_next.serial_int, 101)
|
||||||
|
self.assertEqual(item_prev.serial_int, 99)
|
||||||
|
|
||||||
def test_serialize_stock_invalid(self):
|
def test_serialize_stock_invalid(self):
|
||||||
"""
|
"""
|
||||||
Test manual serialization of parts.
|
Test manual serialization of parts.
|
||||||
|
@ -101,43 +101,16 @@ class StockItemDetail(InvenTreeRoleMixin, DetailView):
|
|||||||
model = StockItem
|
model = StockItem
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
""" add previous and next item """
|
"""
|
||||||
|
Add information on the "next" and "previous" StockItem objects,
|
||||||
|
based on the serial numbers.
|
||||||
|
"""
|
||||||
|
|
||||||
data = super().get_context_data(**kwargs)
|
data = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
if self.object.serialized:
|
if self.object.serialized:
|
||||||
|
data['previous'] = self.object.get_next_serialized_item(reverse=True)
|
||||||
serial_elem = {}
|
data['next'] = self.object.get_next_serialized_item()
|
||||||
|
|
||||||
try:
|
|
||||||
current = int(self.object.serial)
|
|
||||||
|
|
||||||
for item in self.object.part.stock_items.all():
|
|
||||||
|
|
||||||
if item.serialized:
|
|
||||||
try:
|
|
||||||
sn = int(item.serial)
|
|
||||||
serial_elem[sn] = item
|
|
||||||
except ValueError:
|
|
||||||
# We only support integer serial number progression
|
|
||||||
pass
|
|
||||||
|
|
||||||
serials = serial_elem.keys()
|
|
||||||
|
|
||||||
# previous
|
|
||||||
for nbr in range(current - 1, min(serials), -1):
|
|
||||||
if nbr in serials:
|
|
||||||
data['previous'] = serial_elem.get(nbr, None)
|
|
||||||
break
|
|
||||||
|
|
||||||
# next
|
|
||||||
for nbr in range(current + 1, max(serials) + 1):
|
|
||||||
if nbr in serials:
|
|
||||||
data['next'] = serial_elem.get(nbr, None)
|
|
||||||
break
|
|
||||||
|
|
||||||
except ValueError:
|
|
||||||
# We only support integer serial number progression
|
|
||||||
pass
|
|
||||||
|
|
||||||
data['ownership_enabled'] = common.models.InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL')
|
data['ownership_enabled'] = common.models.InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL')
|
||||||
data['item_owner'] = self.object.get_item_owner()
|
data['item_owner'] = self.object.get_item_owner()
|
||||||
|
Loading…
Reference in New Issue
Block a user