Merge branch 'inventree:master' into matmair/issue2694

This commit is contained in:
Matthias Mair 2022-03-08 21:26:21 +01:00 committed by GitHub
commit ec021624cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 20091 additions and 18654 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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()

View File

@ -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:

View File

@ -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.

View File

@ -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()