diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 2825731efc..1ceedf63e2 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -199,7 +199,7 @@ class Build(models.Model): build_item.save() @transaction.atomic - def completeBuild(self, location, user): + def completeBuild(self, location, serial_numbers, user): """ Mark the Build as COMPLETE - Takes allocated items from stock @@ -227,19 +227,36 @@ class Build(models.Model): self.completed_by = user - # Add stock of the newly created item - item = StockItem.objects.create( - part=self.part, - location=location, - quantity=self.quantity, - batch=str(self.batch) if self.batch else '', - notes='Built {q} on {now}'.format( - q=self.quantity, - now=str(datetime.now().date()) - ) + notes = 'Built {q} on {now}'.format( + q=self.quantity, + now=str(datetime.now().date()) ) - item.save() + if self.part.trackable: + # Add new serial numbers + for serial in serial_numbers: + item = StockItem.objects.create( + part=self.part, + location=location, + quantity=1, + serial=serial, + batch=str(self.batch) if self.batch else '', + notes=notes + ) + + item.save() + + else: + # Add stock of the newly created item + item = StockItem.objects.create( + part=self.part, + location=location, + quantity=self.quantity, + batch=str(self.batch) if self.batch else '', + notes=notes + ) + + item.save() # Finally, mark the build as complete self.status = BuildStatus.COMPLETE diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 9919b35a39..d9d3d6abdb 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -225,7 +225,7 @@ class BuildComplete(AjaxUpdateView): build = Build.objects.get(id=self.kwargs['pk']) context = {} - + # Build object context['build'] = build @@ -263,21 +263,35 @@ class BuildComplete(AjaxUpdateView): except StockLocation.DoesNotExist: form.errors['location'] = ['Invalid location selected'] - valid = False + serials = [] - serials = request.POST.get('serial_numbers', '') + if build.part.trackable: + # A build for a trackable part must specify serial numbers - try: - serials = ExtractSerialNumbers(serials, build.quantity) + sn = request.POST.get('serial_numbers', '') - print(serials) + try: + # Exctract a list of provided serial numbers + serials = ExtractSerialNumbers(sn, build.quantity) - except ValidationError as e: - form.errors['serial_numbers'] = e.messages - valid = False + existing = [] + + for serial in serials: + if not StockItem.check_serial_number(build.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 if valid: - build.completeBuild(location, request.user) + build.completeBuild(location, serials, request.user) data = { 'form_valid': valid, diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index e56466c832..1ba6c4d154 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -280,6 +280,7 @@ 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/models.py b/InvenTree/stock/models.py index 11a588075d..39b973f45e 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -92,6 +92,7 @@ class StockItem(models.Model): location: Where this StockItem is located quantity: Number of stocked units batch: Batch number for this StockItem + serial: Unique serial number for this StockItem URL: Optional URL to link to external resource updated: Date that this stock item was last updated (auto) stocktake_date: Date of last stocktake for this item @@ -121,6 +122,32 @@ class StockItem(models.Model): system=True ) + @classmethod + def check_serial_number(cls, part, serial_number): + """ Check if a new stock item can be created with the provided part_id + + Args: + part: The part to be checked + """ + + if not part.trackable: + return False + + items = StockItem.objects.filter(serial=serial_number) + + # Is this part a variant? If so, check S/N across all sibling variants + if part.variant_of is not None: + items = items.filter(part__variant_of=part.variant_of) + else: + items = items.filter(part=part) + + # An existing serial number exists + if items.exists(): + return False + + return True + + def validate_unique(self, exclude=None): super(StockItem, self).validate_unique(exclude) @@ -129,11 +156,18 @@ class StockItem(models.Model): # across all variants of the same template part try: - if self.serial is not None and self.part.variant_of is not None: - if StockItem.objects.filter(part__variant_of=self.part.variant_of, serial=self.serial).exclude(id=self.id).exists(): - raise ValidationError({ - 'serial': _('A part with this serial number already exists for template part {part}'.format(part=self.part.variant_of)) - }) + if self.serial is not None: + # This is a variant part (check S/N across all sibling variants) + if self.part.variant_of is not None: + if StockItem.objects.filter(part__variant_of=self.part.variant_of, serial=self.serial).exclude(id=self.id).exists(): + raise ValidationError({ + 'serial': _('A part with this serial number already exists for template part {part}'.format(part=self.part.variant_of)) + }) + else: + if StockItem.objects.filter(serial=self.serial).exclude(id=self.id).exists(): + raise ValidationError({ + 'serial': _('A part with this serial number already exists') + }) except Part.DoesNotExist: pass