diff --git a/.travis.yml b/.travis.yml index 00d049b7a1..58bdad303f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,8 @@ addons: -sqlite3 before_install: - - make install + - make requirements + - make secret - make migrate script: diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index e56466c832..9e0b2cea29 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -634,7 +634,8 @@ class Part(models.Model): For hash is calculated from the following fields of each BOM item: - Part.full_name (if the part name changes, the BOM checksum is invalidated) - - quantity + - Quantity + - Reference field - Note field returns a string representation of a hash object which can be compared with a stored value @@ -647,6 +648,7 @@ class Part(models.Model): hash.update(str(item.sub_part.full_name).encode()) hash.update(str(item.quantity).encode()) hash.update(str(item.note).encode()) + hash.update(str(item.reference).encode()) return str(hash.digest()) diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index b650f5b33b..8cbae62fec 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -102,7 +102,7 @@

Available Stock

-

{{ part.net_stock }} {{ part.units }}

+

{{ part.available_stock }} {{ part.units }}

In Stock diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index 9e24b538f3..46a50521dd 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -118,11 +118,12 @@ class EditStockItemForm(HelperForm): fields = [ 'supplier_part', + 'serial', 'batch', - 'delete_on_deplete', 'status', 'notes', 'URL', + 'delete_on_deplete', ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 4f4c74d6a2..b5a88d4e66 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -122,6 +122,11 @@ class StockItem(models.Model): system=True ) + @property + def serialized(self): + """ Return True if this StockItem is serialized """ + return self.serial is not None and self.quantity == 1 + @classmethod def check_serial_number(cls, part, serial_number): """ Check if a new stock item can be created with the provided part_id @@ -190,20 +195,21 @@ class StockItem(models.Model): }) if self.part is not None: - # A trackable part must have a serial number - if self.part.trackable: - if not self.serial: - raise ValidationError({'serial': _('Serial number must be set for trackable items')}) + # A part with a serial number MUST have the quantity set to 1 + if self.serial is not None: + if self.quantity > 1: + raise ValidationError({ + 'quantity': _('Quantity must be 1 for item with a serial number'), + 'serial': _('Serial number cannot be set if quantity greater than 1') + }) + + if self.quantity == 0: + raise ValidationError({ + 'quantity': _('Quantity must be 1 for item with a serial number') + }) 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") - }) + raise ValidationError({'delete_on_deplete': _("Must be set to False for item with a serial number")}) # A template part cannot be instantiated as a StockItem if self.part.is_template: @@ -316,7 +322,15 @@ class StockItem(models.Model): infinite = models.BooleanField(default=False) def can_delete(self): - # TODO - Return FALSE if this item cannot be deleted! + """ Can this stock item be deleted? It can NOT be deleted under the following circumstances: + + - Has a serial number and is tracked + - Is installed inside another StockItem + """ + + if self.part.trackable and self.serial is not None: + return False + return True @property @@ -349,6 +363,14 @@ class StockItem(models.Model): track.save() + @transaction.atomic + def serializeStock(self, serials, user): + """ Split this stock item into unique serial numbers. + """ + + # TODO + pass + @transaction.atomic def splitStock(self, quantity, user): """ Split this stock item into two items, in the same location. @@ -363,6 +385,10 @@ class StockItem(models.Model): The new item will have a different StockItem ID, while this will remain the same. """ + # Do not split a serialized part + if self.serialized: + return + # Doesn't make sense for a zero quantity if quantity <= 0: return @@ -377,6 +403,8 @@ class StockItem(models.Model): quantity=quantity, supplier_part=self.supplier_part, location=self.location, + notes=self.notes, + URL=self.URL, batch=self.batch, delete_on_deplete=self.delete_on_deplete ) @@ -412,7 +440,7 @@ class StockItem(models.Model): if location is None: # TODO - Raise appropriate error (cannot move to blank location) return False - elif self.location and (location.pk == self.location.pk): + elif self.location and (location.pk == self.location.pk) and (quantity == self.quantity): # TODO - Raise appropriate error (cannot move to same location) return False @@ -450,12 +478,16 @@ class StockItem(models.Model): - False if the StockItem was deleted """ + # Do not adjust quantity of a serialized part + if self.serialized: + return + if quantity < 0: quantity = 0 self.quantity = quantity - if quantity <= 0 and self.delete_on_deplete: + if quantity <= 0 and self.delete_on_deplete and self.can_delete(): self.delete() return False else: @@ -493,6 +525,10 @@ class StockItem(models.Model): or by manually adding the items to the stock location """ + # Cannot add items to a serialized part + if self.serialized: + return False + quantity = int(quantity) # Ignore amounts that do not make sense @@ -513,6 +549,10 @@ class StockItem(models.Model): """ Remove items from stock """ + # Cannot remove items from a serialized part + if self.serialized: + return False + quantity = int(quantity) if quantity <= 0 or self.infinite: diff --git a/InvenTree/stock/templates/stock/item.html b/InvenTree/stock/templates/stock/item.html index f6847d5ae1..62e46c8f84 100644 --- a/InvenTree/stock/templates/stock/item.html +++ b/InvenTree/stock/templates/stock/item.html @@ -5,11 +5,16 @@

Stock Item Details

+ {% if item.serialized %} +

{{ item.part.full_name}} # {{ item.serial }}

+ {% else %}

{{ item.quantity }} × {{ item.part.full_name }}

+ {% endif %}

{% include "qr_button.html" %} {% if item.in_stock %} + {% if not item.serialized %} @@ -19,6 +24,7 @@ + {% endif %} @@ -34,6 +40,11 @@

+ {% if item.serialized %} +
+ This stock item is serialized - it has a unique serial number and the quantity cannot be adjusted. +
+ {% endif %}
@@ -41,7 +52,10 @@ - + {% if item.belongs_to %} @@ -54,9 +68,9 @@ {% endif %} - {% if item.serial %} + {% if item.serialized %} - + {% else %} diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 7595c550b0..6fbea9d885 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -383,8 +383,8 @@ class StockAdjust(AjaxView, FormMixin): if item.new_quantity <= 0: continue - # Do not move to the same location - if destination == item.location: + # Do not move to the same location (unless the quantity is different) + if destination == item.location and item.new_quantity == item.quantity: continue item.move(destination, note, self.request.user, quantity=int(item.new_quantity)) @@ -429,6 +429,9 @@ class StockItemEdit(AjaxUpdateView): query = query.filter(part=item.part.id) form.fields['supplier_part'].queryset = query + if not item.part.trackable: + form.fields.pop('serial') + return form
Part{{ item.part.full_name }} + {% include "hover_image.html" with image=item.part.image hover=True %} + {{ item.part.full_name }} +
{{ item.location.name }}
SerialSerial Number {{ item.serial }}