mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
commit
7ae7c19a24
@ -131,11 +131,21 @@ def ExtractSerialNumbers(serials, expected_quantity):
|
|||||||
expected_quantity: The number of (unique) serial numbers we expect
|
expected_quantity: The number of (unique) serial numbers we expect
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
serials = serials.strip()
|
||||||
|
|
||||||
groups = re.split("[\s,]+", serials)
|
groups = re.split("[\s,]+", serials)
|
||||||
|
|
||||||
numbers = []
|
numbers = []
|
||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
expected_quantity = int(expected_quantity)
|
||||||
|
except ValueError:
|
||||||
|
raise ValidationError([_("Invalid quantity provided")])
|
||||||
|
|
||||||
|
if len(serials) == 0:
|
||||||
|
raise ValidationError([_("Empty serial number string")])
|
||||||
|
|
||||||
for group in groups:
|
for group in groups:
|
||||||
|
|
||||||
group = group.strip()
|
group = group.strip()
|
||||||
@ -155,34 +165,34 @@ def ExtractSerialNumbers(serials, expected_quantity):
|
|||||||
if a < b:
|
if a < b:
|
||||||
for n in range(a, b + 1):
|
for n in range(a, b + 1):
|
||||||
if n in numbers:
|
if n in numbers:
|
||||||
errors.append('Duplicate serial: {n}'.format(n=n))
|
errors.append(_('Duplicate serial: {n}'.format(n=n)))
|
||||||
else:
|
else:
|
||||||
numbers.append(n)
|
numbers.append(n)
|
||||||
else:
|
else:
|
||||||
errors.append("Invalid group: {g}".format(g=group))
|
errors.append(_("Invalid group: {g}".format(g=group)))
|
||||||
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
errors.append("Invalid group: {g}".format(g=group))
|
errors.append(_("Invalid group: {g}".format(g=group)))
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
errors.append("Invalid group: {g}".format(g=group))
|
errors.append(_("Invalid group: {g}".format(g=group)))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
n = int(group)
|
n = int(group)
|
||||||
if n in numbers:
|
if n in numbers:
|
||||||
errors.append("Duplicate serial: {n}".format(n=n))
|
errors.append(_("Duplicate serial: {n}".format(n=n)))
|
||||||
else:
|
else:
|
||||||
numbers.append(n)
|
numbers.append(n)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
errors.append("Invalid group: {g}".format(g=group))
|
errors.append(_("Invalid group: {g}".format(g=group)))
|
||||||
|
|
||||||
if len(errors) > 0:
|
if len(errors) > 0:
|
||||||
raise ValidationError(errors)
|
raise ValidationError(errors)
|
||||||
|
|
||||||
if len(numbers) == 0:
|
if len(numbers) == 0:
|
||||||
raise ValidationError(["No serial numbers found"])
|
raise ValidationError([_("No serial numbers found")])
|
||||||
|
|
||||||
# The number of extracted serial numbers must match the expected quantity
|
# The number of extracted serial numbers must match the expected quantity
|
||||||
if not expected_quantity == len(numbers):
|
if not expected_quantity == len(numbers):
|
||||||
|
@ -5,6 +5,8 @@ Django Forms for interacting with Build objects
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from InvenTree.forms import HelperForm
|
from InvenTree.forms import HelperForm
|
||||||
from django import forms
|
from django import forms
|
||||||
from .models import Build, BuildItem
|
from .models import Build, BuildItem
|
||||||
@ -31,7 +33,7 @@ class EditBuildForm(HelperForm):
|
|||||||
class ConfirmBuildForm(HelperForm):
|
class ConfirmBuildForm(HelperForm):
|
||||||
""" Form for auto-allocation of stock to a build """
|
""" Form for auto-allocation of stock to a build """
|
||||||
|
|
||||||
confirm = forms.BooleanField(required=False, help_text='Confirm')
|
confirm = forms.BooleanField(required=False, help_text=_('Confirm'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Build
|
model = Build
|
||||||
@ -48,9 +50,9 @@ class CompleteBuildForm(HelperForm):
|
|||||||
help_text='Location of completed parts',
|
help_text='Location of completed parts',
|
||||||
)
|
)
|
||||||
|
|
||||||
serial_numbers = forms.CharField(label='Serial numbers', help_text='Enter unique serial numbers')
|
serial_numbers = forms.CharField(label='Serial numbers', required=False, help_text=_('Enter unique serial numbers (or leave blank)'))
|
||||||
|
|
||||||
confirm = forms.BooleanField(required=False, help_text='Confirm build submission')
|
confirm = forms.BooleanField(required=False, help_text=_('Confirm build completion'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Build
|
model = Build
|
||||||
|
@ -270,25 +270,29 @@ class BuildComplete(AjaxUpdateView):
|
|||||||
|
|
||||||
sn = request.POST.get('serial_numbers', '')
|
sn = request.POST.get('serial_numbers', '')
|
||||||
|
|
||||||
try:
|
sn = str(sn).strip()
|
||||||
# Exctract a list of provided serial numbers
|
|
||||||
serials = ExtractSerialNumbers(sn, build.quantity)
|
|
||||||
|
|
||||||
existing = []
|
# If the user has specified serial numbers, check they are valid
|
||||||
|
if len(sn) > 0:
|
||||||
|
try:
|
||||||
|
# Exctract a list of provided serial numbers
|
||||||
|
serials = ExtractSerialNumbers(sn, build.quantity)
|
||||||
|
|
||||||
for serial in serials:
|
existing = []
|
||||||
if not StockItem.check_serial_number(build.part, serial):
|
|
||||||
existing.append(serial)
|
|
||||||
|
|
||||||
if len(existing) > 0:
|
for serial in serials:
|
||||||
exists = ",".join([str(x) for x in existing])
|
if not StockItem.check_serial_number(build.part, serial):
|
||||||
form.errors['serial_numbers'] = [_('The following serial numbers already exist: ({sn})'.format(sn=exists))]
|
existing.append(serial)
|
||||||
|
|
||||||
|
if len(existing) > 0:
|
||||||
|
exists = ",".join([str(x) for x in existing])
|
||||||
|
form.errors['serial_numbers'] = [_('The following serial numbers already exist: ({sn})'.format(sn=exists))]
|
||||||
|
valid = False
|
||||||
|
|
||||||
|
except ValidationError as e:
|
||||||
|
form.errors['serial_numbers'] = e.messages
|
||||||
valid = False
|
valid = False
|
||||||
|
|
||||||
except ValidationError as e:
|
|
||||||
form.errors['serial_numbers'] = e.messages
|
|
||||||
valid = False
|
|
||||||
|
|
||||||
if valid:
|
if valid:
|
||||||
build.completeBuild(location, serials, request.user)
|
build.completeBuild(location, serials, request.user)
|
||||||
|
|
||||||
|
@ -43,6 +43,7 @@
|
|||||||
name: 'Widget'
|
name: 'Widget'
|
||||||
description: 'A watchamacallit'
|
description: 'A watchamacallit'
|
||||||
category: 7
|
category: 7
|
||||||
|
trackable: true
|
||||||
|
|
||||||
- model: part.part
|
- model: part.part
|
||||||
pk: 50
|
pk: 50
|
||||||
|
@ -843,7 +843,8 @@ class Part(models.Model):
|
|||||||
# Copy the BOM data
|
# Copy the BOM data
|
||||||
if kwargs.get('bom', False):
|
if kwargs.get('bom', False):
|
||||||
for item in other.bom_items.all():
|
for item in other.bom_items.all():
|
||||||
# Point the item to THIS part
|
# Point the item to THIS part.
|
||||||
|
# Set the pk to None so a new entry is created.
|
||||||
item.part = self
|
item.part = self
|
||||||
item.pk = None
|
item.pk = None
|
||||||
item.save()
|
item.save()
|
||||||
|
@ -7,6 +7,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.forms.utils import ErrorDict
|
from django.forms.utils import ErrorDict
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from InvenTree.forms import HelperForm
|
from InvenTree.forms import HelperForm
|
||||||
from .models import StockLocation, StockItem, StockItemTracking
|
from .models import StockLocation, StockItem, StockItemTracking
|
||||||
@ -27,7 +28,7 @@ class EditStockLocationForm(HelperForm):
|
|||||||
class CreateStockItemForm(HelperForm):
|
class CreateStockItemForm(HelperForm):
|
||||||
""" Form for creating a new StockItem """
|
""" Form for creating a new StockItem """
|
||||||
|
|
||||||
serial_numbers = forms.CharField(label='Serial numbers', required=False, help_text='Enter unique serial numbers')
|
serial_numbers = forms.CharField(label='Serial numbers', required=False, help_text=_('Enter unique serial numbers (or leave blank)'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = StockItem
|
model = StockItem
|
||||||
@ -62,6 +63,39 @@ class CreateStockItemForm(HelperForm):
|
|||||||
self._clean_form()
|
self._clean_form()
|
||||||
|
|
||||||
|
|
||||||
|
class SerializeStockForm(forms.ModelForm):
|
||||||
|
""" Form for serializing a StockItem. """
|
||||||
|
|
||||||
|
destination = forms.ChoiceField(label='Destination', required=True, help_text='Destination for serialized stock (by default, will remain in current location)')
|
||||||
|
serial_numbers = forms.CharField(label='Serial numbers', required=True, help_text='Unique serial numbers (must match quantity)')
|
||||||
|
note = forms.CharField(label='Notes', required=False, help_text='Add transaction note (optional)')
|
||||||
|
|
||||||
|
def get_location_choices(self):
|
||||||
|
locs = StockLocation.objects.all()
|
||||||
|
|
||||||
|
choices = [(None, '---------')]
|
||||||
|
|
||||||
|
for loc in locs:
|
||||||
|
choices.append((loc.pk, loc.pathstring + ' - ' + loc.description))
|
||||||
|
|
||||||
|
return choices
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.fields['destination'].choices = self.get_location_choices()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = StockItem
|
||||||
|
|
||||||
|
fields = [
|
||||||
|
'quantity',
|
||||||
|
'serial_numbers',
|
||||||
|
'destination',
|
||||||
|
'note',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class AdjustStockForm(forms.ModelForm):
|
class AdjustStockForm(forms.ModelForm):
|
||||||
""" Form for performing simple stock adjustments.
|
""" Form for performing simple stock adjustments.
|
||||||
|
|
||||||
|
@ -138,6 +138,12 @@ class StockItem(models.Model):
|
|||||||
if not part.trackable:
|
if not part.trackable:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Return False if an invalid serial number is supplied
|
||||||
|
try:
|
||||||
|
serial_number = int(serial_number)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
items = StockItem.objects.filter(serial=serial_number)
|
items = StockItem.objects.filter(serial=serial_number)
|
||||||
|
|
||||||
# Is this part a variant? If so, check S/N across all sibling variants
|
# Is this part a variant? If so, check S/N across all sibling variants
|
||||||
@ -204,12 +210,15 @@ class StockItem(models.Model):
|
|||||||
})
|
})
|
||||||
|
|
||||||
if self.quantity == 0:
|
if self.quantity == 0:
|
||||||
|
self.quantity = 1
|
||||||
|
|
||||||
|
elif self.quantity > 1:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'quantity': _('Quantity must be 1 for item with a serial number')
|
'quantity': _('Quantity must be 1 for item with a serial number')
|
||||||
})
|
})
|
||||||
|
|
||||||
if self.delete_on_deplete:
|
# Serial numbered items cannot be deleted on depletion
|
||||||
raise ValidationError({'delete_on_deplete': _("Must be set to False for item with a serial number")})
|
self.delete_on_deplete = False
|
||||||
|
|
||||||
# A template part cannot be instantiated as a StockItem
|
# A template part cannot be instantiated as a StockItem
|
||||||
if self.part.is_template:
|
if self.part.is_template:
|
||||||
@ -350,6 +359,7 @@ class StockItem(models.Model):
|
|||||||
|
|
||||||
Brief automated note detailing a movement or quantity change.
|
Brief automated note detailing a movement or quantity change.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
track = StockItemTracking.objects.create(
|
track = StockItemTracking.objects.create(
|
||||||
item=self,
|
item=self,
|
||||||
title=title,
|
title=title,
|
||||||
@ -364,12 +374,88 @@ class StockItem(models.Model):
|
|||||||
track.save()
|
track.save()
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def serializeStock(self, serials, user):
|
def serializeStock(self, quantity, serials, user, notes='', location=None):
|
||||||
""" Split this stock item into unique serial numbers.
|
""" Split this stock item into unique serial numbers.
|
||||||
|
|
||||||
|
- Quantity can be less than or equal to the quantity of the stock item
|
||||||
|
- Number of serial numbers must match the quantity
|
||||||
|
- Provided serial numbers must not already be in use
|
||||||
|
|
||||||
|
Args:
|
||||||
|
quantity: Number of items to serialize (integer)
|
||||||
|
serials: List of serial numbers (list<int>)
|
||||||
|
user: User object associated with action
|
||||||
|
notes: Optional notes for tracking
|
||||||
|
location: If specified, serialized items will be placed in the given location
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# TODO
|
# Cannot serialize stock that is already serialized!
|
||||||
pass
|
if self.serialized:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Quantity must be a valid integer value
|
||||||
|
try:
|
||||||
|
quantity = int(quantity)
|
||||||
|
except ValueError:
|
||||||
|
raise ValidationError({"quantity": _("Quantity must be integer")})
|
||||||
|
|
||||||
|
if quantity <= 0:
|
||||||
|
raise ValidationError({"quantity": _("Quantity must be greater than zero")})
|
||||||
|
|
||||||
|
if quantity > self.quantity:
|
||||||
|
raise ValidationError({"quantity": _("Quantity must not exceed available stock quantity ({n})".format(n=self.quantity))})
|
||||||
|
|
||||||
|
if not type(serials) in [list, tuple]:
|
||||||
|
raise ValidationError({"serial_numbers": _("Serial numbers must be a list of integers")})
|
||||||
|
|
||||||
|
if any([type(i) is not int for i in serials]):
|
||||||
|
raise ValidationError({"serial_numbers": _("Serial numbers must be a list of integers")})
|
||||||
|
|
||||||
|
if not quantity == len(serials):
|
||||||
|
raise ValidationError({"quantity": _("Quantity does not match serial numbers")})
|
||||||
|
|
||||||
|
# Test if each of the serial numbers are valid
|
||||||
|
existing = []
|
||||||
|
|
||||||
|
for serial in serials:
|
||||||
|
if not StockItem.check_serial_number(self.part, serial):
|
||||||
|
existing.append(serial)
|
||||||
|
|
||||||
|
if len(existing) > 0:
|
||||||
|
raise ValidationError({"serial_numbers": _("Serial numbers already exist: ") + str(existing)})
|
||||||
|
|
||||||
|
# Create a new stock item for each unique serial number
|
||||||
|
for serial in serials:
|
||||||
|
|
||||||
|
# Create a copy of this StockItem
|
||||||
|
new_item = StockItem.objects.get(pk=self.pk)
|
||||||
|
new_item.quantity = 1
|
||||||
|
new_item.serial = serial
|
||||||
|
new_item.pk = None
|
||||||
|
|
||||||
|
if location:
|
||||||
|
new_item.location = location
|
||||||
|
|
||||||
|
new_item.save()
|
||||||
|
|
||||||
|
# Copy entire transaction history
|
||||||
|
new_item.copyHistoryFrom(self)
|
||||||
|
|
||||||
|
# Create a new stock tracking item
|
||||||
|
new_item.addTransactionNote(_('Add serial number'), user, notes=notes)
|
||||||
|
|
||||||
|
# Remove the equivalent number of items
|
||||||
|
self.take_stock(quantity, user, notes=_('Serialized {n} items'.format(n=quantity)))
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def copyHistoryFrom(self, other):
|
||||||
|
""" Copy stock history from another part """
|
||||||
|
|
||||||
|
for item in other.tracking_info.all():
|
||||||
|
|
||||||
|
item.item = self
|
||||||
|
item.pk = None
|
||||||
|
item.save()
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def splitStock(self, quantity, user):
|
def splitStock(self, quantity, user):
|
||||||
@ -411,6 +497,9 @@ class StockItem(models.Model):
|
|||||||
|
|
||||||
new_stock.save()
|
new_stock.save()
|
||||||
|
|
||||||
|
# Copy the transaction history
|
||||||
|
new_stock.copyHistoryFrom(self)
|
||||||
|
|
||||||
# Add a new tracking item for the new stock item
|
# Add a new tracking item for the new stock item
|
||||||
new_stock.addTransactionNote(
|
new_stock.addTransactionNote(
|
||||||
"Split from existing stock",
|
"Split from existing stock",
|
||||||
|
@ -24,6 +24,11 @@
|
|||||||
<button type='button' class='btn btn-default btn-glyph' id='stock-count' title='Count stock'>
|
<button type='button' class='btn btn-default btn-glyph' id='stock-count' title='Count stock'>
|
||||||
<span class='glyphicon glyphicon-ok-circle'/>
|
<span class='glyphicon glyphicon-ok-circle'/>
|
||||||
</button>
|
</button>
|
||||||
|
{% if item.part.trackable %}
|
||||||
|
<button type='button' class='btn btn-default btn-glyph' id='stock-serialize' title='Serialize stock'>
|
||||||
|
<span class='glyphicon glyphicon-th-list'/>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<button type='button' class='btn btn-default btn-glyph' id='stock-move' title='Transfer stock'>
|
<button type='button' class='btn btn-default btn-glyph' id='stock-move' title='Transfer stock'>
|
||||||
<span class='glyphicon glyphicon-transfer' style='color: #11a;'/>
|
<span class='glyphicon glyphicon-transfer' style='color: #11a;'/>
|
||||||
@ -162,6 +167,15 @@
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#stock-serialize").click(function() {
|
||||||
|
launchModalForm(
|
||||||
|
"{% url 'stock-item-serialize' item.id %}",
|
||||||
|
{
|
||||||
|
reload: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
$("#stock-duplicate").click(function() {
|
$("#stock-duplicate").click(function() {
|
||||||
launchModalForm(
|
launchModalForm(
|
||||||
"{% url 'stock-item-create' %}",
|
"{% url 'stock-item-create' %}",
|
||||||
|
6
InvenTree/stock/templates/stock/item_serialize.html
Normal file
6
InvenTree/stock/templates/stock/item_serialize.html
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{% extends "modal_form.html" %}
|
||||||
|
|
||||||
|
{% block pre_form_content %}
|
||||||
|
Create serialized items from this stock item.<br>
|
||||||
|
Select quantity to serialize, and unique serial numbers.
|
||||||
|
{% endblock %}
|
@ -1,5 +1,7 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.db.models import Sum
|
from django.db.models import Sum
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
from .models import StockLocation, StockItem, StockItemTracking
|
from .models import StockLocation, StockItem, StockItemTracking
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
@ -28,6 +30,14 @@ class StockTest(TestCase):
|
|||||||
self.drawer2 = StockLocation.objects.get(name='Drawer_2')
|
self.drawer2 = StockLocation.objects.get(name='Drawer_2')
|
||||||
self.drawer3 = StockLocation.objects.get(name='Drawer_3')
|
self.drawer3 = StockLocation.objects.get(name='Drawer_3')
|
||||||
|
|
||||||
|
# Create a user
|
||||||
|
User = get_user_model()
|
||||||
|
User.objects.create_user('username', 'user@email.com', 'password')
|
||||||
|
|
||||||
|
self.client.login(username='username', password='password')
|
||||||
|
|
||||||
|
self.user = User.objects.get(username='username')
|
||||||
|
|
||||||
def test_loc_count(self):
|
def test_loc_count(self):
|
||||||
self.assertEqual(StockLocation.objects.count(), 7)
|
self.assertEqual(StockLocation.objects.count(), 7)
|
||||||
|
|
||||||
@ -244,3 +254,71 @@ class StockTest(TestCase):
|
|||||||
|
|
||||||
with self.assertRaises(StockItem.DoesNotExist):
|
with self.assertRaises(StockItem.DoesNotExist):
|
||||||
w2 = StockItem.objects.get(pk=101)
|
w2 = StockItem.objects.get(pk=101)
|
||||||
|
|
||||||
|
def test_serialize_stock_invalid(self):
|
||||||
|
"""
|
||||||
|
Test manual serialization of parts.
|
||||||
|
Each of these tests should fail
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Test serialization of non-serializable part
|
||||||
|
item = StockItem.objects.get(pk=1234)
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
item.serializeStock(5, [1, 2, 3, 4, 5], self.user)
|
||||||
|
|
||||||
|
# Pick a StockItem which can actually be serialized
|
||||||
|
item = StockItem.objects.get(pk=100)
|
||||||
|
|
||||||
|
# Try an invalid quantity
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
item.serializeStock("k", [], self.user)
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
item.serializeStock(-1, [], self.user)
|
||||||
|
|
||||||
|
# Try invalid serial numbers
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
item.serializeStock(3, [1, 2, 'k'], self.user)
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
item.serializeStock(3, "hello", self.user)
|
||||||
|
|
||||||
|
def test_seiralize_stock_valid(self):
|
||||||
|
""" Perform valid stock serializations """
|
||||||
|
|
||||||
|
# There are 10 of these in stock
|
||||||
|
# Item will deplete when deleted
|
||||||
|
item = StockItem.objects.get(pk=100)
|
||||||
|
item.delete_on_deplete = True
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
n = StockItem.objects.filter(part=25).count()
|
||||||
|
|
||||||
|
self.assertEqual(item.quantity, 10)
|
||||||
|
|
||||||
|
item.serializeStock(3, [1, 2, 3], self.user)
|
||||||
|
|
||||||
|
self.assertEqual(item.quantity, 7)
|
||||||
|
|
||||||
|
# Try to serialize again (with same serial numbers)
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
item.serializeStock(3, [1, 2, 3], self.user)
|
||||||
|
|
||||||
|
# Try to serialize too many items
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
item.serializeStock(13, [1, 2, 3], self.user)
|
||||||
|
|
||||||
|
# Serialize some more stock
|
||||||
|
item.serializeStock(5, [6, 7, 8, 9, 10], self.user)
|
||||||
|
|
||||||
|
self.assertEqual(item.quantity, 2)
|
||||||
|
|
||||||
|
# There should be 8 more items now
|
||||||
|
self.assertEqual(StockItem.objects.filter(part=25).count(), n + 8)
|
||||||
|
|
||||||
|
# Serialize the remainder of the stock
|
||||||
|
item.serializeStock(2, [99, 100], self.user)
|
||||||
|
|
||||||
|
# Two more items but the original has been deleted
|
||||||
|
self.assertEqual(StockItem.objects.filter(part=25).count(), n + 9)
|
||||||
|
@ -17,11 +17,12 @@ stock_location_detail_urls = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
stock_item_detail_urls = [
|
stock_item_detail_urls = [
|
||||||
url(r'^edit/?', views.StockItemEdit.as_view(), name='stock-item-edit'),
|
url(r'^edit/', views.StockItemEdit.as_view(), name='stock-item-edit'),
|
||||||
url(r'^delete/?', views.StockItemDelete.as_view(), name='stock-item-delete'),
|
url(r'^serialize/', views.StockItemSerialize.as_view(), name='stock-item-serialize'),
|
||||||
url(r'^qr_code/?', views.StockItemQRCode.as_view(), name='stock-item-qr'),
|
url(r'^delete/', views.StockItemDelete.as_view(), name='stock-item-delete'),
|
||||||
|
url(r'^qr_code/', views.StockItemQRCode.as_view(), name='stock-item-qr'),
|
||||||
|
|
||||||
url(r'^add_tracking/?', views.StockItemTrackingCreate.as_view(), name='stock-tracking-create'),
|
url(r'^add_tracking/', views.StockItemTrackingCreate.as_view(), name='stock-tracking-create'),
|
||||||
|
|
||||||
url('^.*$', views.StockItemDetail.as_view(), name='stock-item-detail'),
|
url('^.*$', views.StockItemDetail.as_view(), name='stock-item-detail'),
|
||||||
]
|
]
|
||||||
|
@ -29,6 +29,7 @@ from .forms import CreateStockItemForm
|
|||||||
from .forms import EditStockItemForm
|
from .forms import EditStockItemForm
|
||||||
from .forms import AdjustStockForm
|
from .forms import AdjustStockForm
|
||||||
from .forms import TrackingEntryForm
|
from .forms import TrackingEntryForm
|
||||||
|
from .forms import SerializeStockForm
|
||||||
|
|
||||||
|
|
||||||
class StockIndex(ListView):
|
class StockIndex(ListView):
|
||||||
@ -461,12 +462,84 @@ class StockLocationCreate(AjaxCreateView):
|
|||||||
return initials
|
return initials
|
||||||
|
|
||||||
|
|
||||||
|
class StockItemSerialize(AjaxUpdateView):
|
||||||
|
""" View for manually serializing a StockItem """
|
||||||
|
|
||||||
|
model = StockItem
|
||||||
|
ajax_template_name = 'stock/item_serialize.html'
|
||||||
|
ajax_form_title = 'Serialize Stock'
|
||||||
|
form_class = SerializeStockForm
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
|
||||||
|
initials = super().get_initial().copy()
|
||||||
|
|
||||||
|
item = self.get_object()
|
||||||
|
|
||||||
|
initials['quantity'] = item.quantity
|
||||||
|
initials['destination'] = item.location.pk
|
||||||
|
|
||||||
|
return initials
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
|
||||||
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
|
||||||
|
form = self.get_form()
|
||||||
|
|
||||||
|
item = self.get_object()
|
||||||
|
|
||||||
|
quantity = request.POST.get('quantity', None)
|
||||||
|
serials = request.POST.get('serial_numbers', '')
|
||||||
|
dest_id = request.POST.get('destination', None)
|
||||||
|
notes = request.POST.get('note', None)
|
||||||
|
user = request.user
|
||||||
|
|
||||||
|
valid = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
destination = StockLocation.objects.get(pk=dest_id)
|
||||||
|
except (ValueError, StockLocation.DoesNotExist):
|
||||||
|
destination = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
numbers = ExtractSerialNumbers(serials, quantity)
|
||||||
|
except ValidationError as e:
|
||||||
|
form.errors['serial_numbers'] = e.messages
|
||||||
|
valid = False
|
||||||
|
|
||||||
|
if valid:
|
||||||
|
try:
|
||||||
|
item.serializeStock(quantity, numbers, user, notes=notes, location=destination)
|
||||||
|
except ValidationError as e:
|
||||||
|
messages = e.message_dict
|
||||||
|
|
||||||
|
for k in messages.keys():
|
||||||
|
if k in ['quantity', 'destionation', 'serial_numbers']:
|
||||||
|
form.errors[k] = messages[k]
|
||||||
|
else:
|
||||||
|
form.non_field_errors = messages[k]
|
||||||
|
|
||||||
|
valid = False
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'form_valid': valid,
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.renderJsonResponse(request, form, data=data)
|
||||||
|
|
||||||
|
|
||||||
class StockItemCreate(AjaxCreateView):
|
class StockItemCreate(AjaxCreateView):
|
||||||
"""
|
"""
|
||||||
View for creating a new StockItem
|
View for creating a new StockItem
|
||||||
Parameters can be pre-filled by passing query items:
|
Parameters can be pre-filled by passing query items:
|
||||||
- part: The part of which the new StockItem is an instance
|
- part: The part of which the new StockItem is an instance
|
||||||
- location: The location of the new StockItem
|
- location: The location of the new StockItem
|
||||||
|
|
||||||
|
If the parent part is a "tracked" part, provide an option to create uniquely serialized items
|
||||||
|
rather than a bulk quantity of stock items
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model = StockItem
|
model = StockItem
|
||||||
@ -593,47 +666,51 @@ class StockItemCreate(AjaxCreateView):
|
|||||||
if part.trackable:
|
if part.trackable:
|
||||||
sn = request.POST.get('serial_numbers', '')
|
sn = request.POST.get('serial_numbers', '')
|
||||||
|
|
||||||
try:
|
sn = str(sn).strip()
|
||||||
serials = ExtractSerialNumbers(sn, quantity)
|
|
||||||
|
|
||||||
existing = []
|
# If user has specified a range of serial numbers
|
||||||
|
if len(sn) > 0:
|
||||||
|
try:
|
||||||
|
serials = ExtractSerialNumbers(sn, quantity)
|
||||||
|
|
||||||
for serial in serials:
|
existing = []
|
||||||
if not StockItem.check_serial_number(part, serial):
|
|
||||||
existing.append(serial)
|
|
||||||
|
|
||||||
if len(existing) > 0:
|
for serial in serials:
|
||||||
exists = ",".join([str(x) for x in existing])
|
if not StockItem.check_serial_number(part, serial):
|
||||||
form.errors['serial_numbers'] = [_('The following serial numbers already exist: ({sn})'.format(sn=exists))]
|
existing.append(serial)
|
||||||
|
|
||||||
|
if len(existing) > 0:
|
||||||
|
exists = ",".join([str(x) for x in existing])
|
||||||
|
form.errors['serial_numbers'] = [_('The following serial numbers already exist: ({sn})'.format(sn=exists))]
|
||||||
|
valid = False
|
||||||
|
|
||||||
|
# At this point we have a list of serial numbers which we know are valid,
|
||||||
|
# and do not currently exist
|
||||||
|
form.clean()
|
||||||
|
|
||||||
|
data = form.cleaned_data
|
||||||
|
|
||||||
|
for serial in serials:
|
||||||
|
# Create a new stock item for each serial number
|
||||||
|
item = StockItem(
|
||||||
|
part=part,
|
||||||
|
quantity=1,
|
||||||
|
serial=serial,
|
||||||
|
supplier_part=data.get('supplier_part'),
|
||||||
|
location=data.get('location'),
|
||||||
|
batch=data.get('batch'),
|
||||||
|
delete_on_deplete=False,
|
||||||
|
status=data.get('status'),
|
||||||
|
notes=data.get('notes'),
|
||||||
|
URL=data.get('URL'),
|
||||||
|
)
|
||||||
|
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
except ValidationError as e:
|
||||||
|
form.errors['serial_numbers'] = e.messages
|
||||||
valid = False
|
valid = False
|
||||||
|
|
||||||
# At this point we have a list of serial numbers which we know are valid,
|
|
||||||
# and do not currently exist
|
|
||||||
form.clean()
|
|
||||||
|
|
||||||
data = form.cleaned_data
|
|
||||||
|
|
||||||
for serial in serials:
|
|
||||||
# Create a new stock item for each serial number
|
|
||||||
item = StockItem(
|
|
||||||
part=part,
|
|
||||||
quantity=1,
|
|
||||||
serial=serial,
|
|
||||||
supplier_part=data.get('supplier_part'),
|
|
||||||
location=data.get('location'),
|
|
||||||
batch=data.get('batch'),
|
|
||||||
delete_on_deplete=False,
|
|
||||||
status=data.get('status'),
|
|
||||||
notes=data.get('notes'),
|
|
||||||
URL=data.get('URL'),
|
|
||||||
)
|
|
||||||
|
|
||||||
item.save()
|
|
||||||
|
|
||||||
except ValidationError as e:
|
|
||||||
form.errors['serial_numbers'] = e.messages
|
|
||||||
valid = False
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# For non-serialized items, simply save the form.
|
# For non-serialized items, simply save the form.
|
||||||
# We need to call _post_clean() here because it is prevented in the form implementation
|
# We need to call _post_clean() here because it is prevented in the form implementation
|
||||||
|
Loading…
Reference in New Issue
Block a user