From 9e5eadd6c37a93e1a8de79742b62b5833cb55544 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 23 Jul 2019 10:31:34 +1000 Subject: [PATCH] Set serial numbers when creating a new stock item --- InvenTree/InvenTree/helpers.py | 2 +- InvenTree/build/views.py | 3 +- InvenTree/part/models.py | 1 - InvenTree/stock/forms.py | 24 +++++++++++- InvenTree/stock/models.py | 29 +++++++------- InvenTree/stock/views.py | 69 +++++++++++++++++++++++++++++++++- 6 files changed, 105 insertions(+), 23 deletions(-) diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index fd31bfad5c..f4e8f8b2e9 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -188,4 +188,4 @@ def ExtractSerialNumbers(serials, expected_quantity): if not expected_quantity == len(numbers): raise ValidationError([_("Number of unique serial number ({s}) must match quantity ({q})".format(s=len(numbers), q=expected_quantity))]) - return numbers \ No newline at end of file + return numbers diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index d9d3d6abdb..db276ed88d 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -282,10 +282,9 @@ class BuildComplete(AjaxUpdateView): 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))] + 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 diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 1ba6c4d154..e56466c832 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -280,7 +280,6 @@ class Part(models.Model): else: return static('/img/blank_image.png') - def validate_unique(self, exclude=None): """ Validate that a part is 'unique'. Uniqueness is checked across the following (case insensitive) fields: diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index d1fd841612..9e24b538f3 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -6,8 +6,9 @@ Django Forms for interacting with Stock app from __future__ import unicode_literals from django import forms -from InvenTree.forms import HelperForm +from django.forms.utils import ErrorDict +from InvenTree.forms import HelperForm from .models import StockLocation, StockItem, StockItemTracking @@ -26,6 +27,8 @@ class EditStockLocationForm(HelperForm): class CreateStockItemForm(HelperForm): """ Form for creating a new StockItem """ + serial_numbers = forms.CharField(label='Serial numbers', required=False, help_text='Enter unique serial numbers') + class Meta: model = StockItem fields = [ @@ -34,13 +37,30 @@ class CreateStockItemForm(HelperForm): 'location', 'quantity', 'batch', - 'serial', + 'serial_numbers', 'delete_on_deplete', 'status', 'notes', 'URL', ] + # Custom clean to prevent complex StockItem.clean() logic from running (yet) + def full_clean(self): + self._errors = ErrorDict() + + if not self.is_bound: # Stop further processing. + return + + self.cleaned_data = {} + # If the form is permitted to be empty, and none of the form data has + # changed from the initial data, short circuit any validation. + if self.empty_permitted and not self.has_changed(): + return + + # Don't run _post_clean() as this will run StockItem.clean() + self._clean_fields() + self._clean_form() + class AdjustStockForm(forms.ModelForm): """ Form for performing simple stock adjustments. diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 39b973f45e..7695a0e3ae 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -147,7 +147,6 @@ class StockItem(models.Model): return True - def validate_unique(self, exclude=None): super(StockItem, self).validate_unique(exclude) @@ -192,16 +191,23 @@ class StockItem(models.Model): if self.part is not None: # A trackable part must have a serial number - if self.part.trackable and not self.serial: - raise ValidationError({ - 'serial': _('Serial number must be set for trackable items') - }) + if self.part.trackable: + if not self.serial: + raise ValidationError({'serial': _('Serial number must be set for trackable items')}) + + if self.delete_on_deplete: + raise ValidationError({'delete_on_deplete': _("Must be set to False for trackable items")}) + + # Serial number cannot be set for items with quantity greater than 1 + if not self.quantity == 1: + raise ValidationError({ + 'quantity': _("Quantity must be set to 1 for item with a serial number"), + 'serial': _("Serial number cannot be set if quantity > 1") + }) # A template part cannot be instantiated as a StockItem if self.part.is_template: - raise ValidationError({ - 'part': _('Stock item cannot be created for a template Part') - }) + raise ValidationError({'part': _('Stock item cannot be created for a template Part')}) except Part.DoesNotExist: # This gets thrown if self.supplier_part is null @@ -213,13 +219,6 @@ class StockItem(models.Model): 'belongs_to': _('Item cannot belong to itself') }) - # Serial number cannot be set for items with quantity greater than 1 - if not self.quantity == 1 and self.serial: - raise ValidationError({ - 'quantity': _("Quantity must be set to 1 for item with a serial number"), - 'serial': _("Serial number cannot be set if quantity > 1") - }) - def get_absolute_url(self): return reverse('stock-item-detail', kwargs={'pk': self.id}) diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index edaf7a5946..a2b1bf87e9 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -5,6 +5,7 @@ Django views for interacting with Stock app # -*- coding: utf-8 -*- from __future__ import unicode_literals +from django.core.exceptions import ValidationError from django.views.generic.edit import FormMixin from django.views.generic import DetailView, ListView from django.forms.models import model_to_dict @@ -17,6 +18,7 @@ from InvenTree.views import AjaxUpdateView, AjaxDeleteView, AjaxCreateView from InvenTree.views import QRCodeView from InvenTree.helpers import str2bool +from InvenTree.helpers import ExtractSerialNumbers from datetime import datetime from part.models import Part @@ -476,7 +478,7 @@ class StockItemCreate(AjaxCreateView): ForeignKey choices based on other selections """ - form = super(AjaxCreateView, self).get_form() + form = super().get_form() # If the user has selected a Part, limit choices for SupplierPart if form['part'].value(): @@ -488,10 +490,16 @@ class StockItemCreate(AjaxCreateView): # Hide the 'part' field (as a valid part is selected) form.fields['part'].widget = HiddenInput() + # trackable parts get special consideration + if part.trackable: + form.fields['delete_on_deplete'].widget = HiddenInput() + form.fields['delete_on_deplete'].initial = False + else: + form.fields.pop('serial_numbers') + # If the part is NOT purchaseable, hide the supplier_part field if not part.purchaseable: form.fields['supplier_part'].widget = HiddenInput() - else: # Pre-select the allowable SupplierPart options parts = form.fields['supplier_part'].queryset @@ -555,6 +563,63 @@ class StockItemCreate(AjaxCreateView): return initials + def post(self, request, *args, **kwargs): + """ Handle POST of StockItemCreate form. + + - Manage serial-number valdiation for tracked parts + """ + + form = self.get_form() + + valid = form.is_valid() + + if valid: + part_id = form['part'].value() + try: + part = Part.objects.get(id=part_id) + quantity = int(form['quantity'].value()) + except (Part.DoesNotExist, ValueError): + part = None + quantity = 1 + valid = False + + if part is None: + form.errors['part'] = [_('Invalid part selection')] + else: + # A trackable part must provide serial numbesr + if part.trackable: + sn = request.POST.get('serial_numbers', '') + + try: + serials = ExtractSerialNumbers(sn, quantity) + + existing = [] + + for serial in serials: + if not StockItem.check_serial_number(part, serial): + 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 + + print("Valid?", valid) + + valid = False + + print("valid:", valid) + + data = { + 'form_valid': valid, + } + + return self.renderJsonResponse(request, form, data=data) + class StockLocationDelete(AjaxDeleteView): """