From 33fe4d186d375ce1f4d1108297a3444b20d9217d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 24 Jul 2019 19:48:37 +1000 Subject: [PATCH 01/13] Include more fields when splitting stock --- InvenTree/stock/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 4f4c74d6a2..359e9a0115 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -377,6 +377,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 ) From 3058b895dd5ed2dea0ab5f5d6decf5c97fba4984 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 24 Jul 2019 20:24:12 +1000 Subject: [PATCH 02/13] Prevent auto-delete of stock items which have a serial number --- InvenTree/stock/models.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 359e9a0115..c92776a2fa 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -316,7 +316,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 part.trackable and self.serial is not None: + return False + return True @property @@ -457,7 +465,7 @@ class StockItem(models.Model): 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: From 42e1370e92d1853768477d33e88a32fe3bf602fb Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 25 Jul 2019 10:36:59 +1000 Subject: [PATCH 03/13] Bug fix --- InvenTree/stock/models.py | 2 +- InvenTree/stock/views.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index c92776a2fa..abc4702e4a 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -322,7 +322,7 @@ class StockItem(models.Model): - Is installed inside another StockItem """ - if part.trackable and self.serial is not None: + if self.part.trackable and self.serial is not None: return False return True diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 7595c550b0..b7fbb9f9e4 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)) From 94c0102742b2729ffdbec15166a1f744ac8866a6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 25 Jul 2019 11:04:45 +1000 Subject: [PATCH 04/13] Improve validation logic for StockItem - Allow tracked items to exist without a serial number (e.g. non-serialized tracked items) --- InvenTree/stock/forms.py | 3 ++- InvenTree/stock/models.py | 27 ++++++++++++++------------- 2 files changed, 16 insertions(+), 14 deletions(-) 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 abc4702e4a..446ab65a56 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -190,20 +190,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: @@ -422,7 +423,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 From fe7392f152e618a8c34ef9e765e7a0a25b2960af Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 25 Jul 2019 11:05:09 +1000 Subject: [PATCH 05/13] Prevent stock adjustments for serialized stock items --- InvenTree/stock/models.py | 29 +++++++++++++++++++++++ InvenTree/stock/templates/stock/item.html | 15 ++++++++++-- InvenTree/stock/views.py | 3 +++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 446ab65a56..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 @@ -358,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. @@ -372,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 @@ -461,6 +478,10 @@ 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 @@ -504,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 @@ -524,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..97895fd4d2 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 %}
@@ -54,9 +65,9 @@ {{ item.location.name }} {% endif %} - {% if item.serial %} + {% if item.serialized %} - Serial + Serial Number {{ item.serial }} {% else %} diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index b7fbb9f9e4..6fbea9d885 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -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 From c8bf20ad41040161a637f0bb0771d3d9f71c57c3 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 25 Jul 2019 11:08:22 +1000 Subject: [PATCH 06/13] Add part hover image --- InvenTree/stock/templates/stock/item.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/InvenTree/stock/templates/stock/item.html b/InvenTree/stock/templates/stock/item.html index 97895fd4d2..62e46c8f84 100644 --- a/InvenTree/stock/templates/stock/item.html +++ b/InvenTree/stock/templates/stock/item.html @@ -52,7 +52,10 @@ - + {% if item.belongs_to %} From 5ee07eae7c6a7cb9a83ac35a5751c2e799f1b56f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 1 Aug 2019 20:07:06 +1000 Subject: [PATCH 07/13] Update installation routine and docs - Single call of "make install" does everything - Add backup_dir option to yaml config file --- InvenTree/InvenTree/settings.py | 4 +++- InvenTree/config.yaml | 6 +++++- Makefile | 6 +++++- docs/config.rst | 7 ++++++- docs/start.rst | 13 +++++++------ 5 files changed, 26 insertions(+), 10 deletions(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 42d9a4b243..cd55fc1b25 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -231,4 +231,6 @@ IMPORT_EXPORT_USE_TRANSACTIONS = True # Settings for dbbsettings app DBBACKUP_STORAGE = 'django.core.files.storage.FileSystemStorage' -DBBACKUP_STORAGE_OPTIONS = {'location': tempfile.gettempdir()} +DBBACKUP_STORAGE_OPTIONS = { + 'location': CONFIG.get('backup_dir', tempfile.gettempdir()), +} diff --git a/InvenTree/config.yaml b/InvenTree/config.yaml index 5aa24a3e69..510951842a 100644 --- a/InvenTree/config.yaml +++ b/InvenTree/config.yaml @@ -43,4 +43,8 @@ media_root: './media' static_root: './static' # Logging options -log_queries: False \ No newline at end of file +log_queries: False + +# Backup options +# Set the backup_dir parameter to store backup files in a specific location +# backup_dir = "/home/me/inventree-backup/" \ No newline at end of file diff --git a/Makefile b/Makefile index cdef6b87c4..85acd5850c 100644 --- a/Makefile +++ b/Makefile @@ -16,13 +16,17 @@ migrate: python3 InvenTree/manage.py migrate --run-syncdb python3 InvenTree/manage.py check -install: +requirements: pip3 install -U -r requirements.txt + +secret: python3 InvenTree/keygen.py superuser: python3 InvenTree/manage.py createsuperuser +install: requirements secret migrate superuser + style: flake8 InvenTree diff --git a/docs/config.rst b/docs/config.rst index d8388ae155..fc768b8e87 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -47,4 +47,9 @@ For further information, refer to the following documentation: Uploaded File Storage --------------------- -By default, uploaded files are stored in the local direction ``./media``. This directory should be changed based on the particular installation requirements. \ No newline at end of file +By default, uploaded files are stored in the local direction ``./media``. This directory should be changed based on the particular installation requirements. + +Backup Location +--------------- + +The default behaviour of the database backup is to generate backup files for database tables and media files to the user's temporary directory. The target directory can be overridden by setting the *backup_dir* parameter in the config file. \ No newline at end of file diff --git a/docs/start.rst b/docs/start.rst index 829d051736..9fe52868aa 100644 --- a/docs/start.rst +++ b/docs/start.rst @@ -24,6 +24,8 @@ which performs the following actions: * Installs all required Python packages using pip package manager * Generates a SECREY_KEY file required for the django authentication framework +* Performs initial database installation and migrations +* Prompts user to create a superuser account Install Configuration --------------------- @@ -34,15 +36,10 @@ The configuration file provides administrators control over various setup option For further information on installation configuration, refer to the `Configuration `_ section. -Superuser Account ------------------ - -Run ``make superuser`` to create a superuser account, required for initial system login. - Run Development Server ---------------------- -Run ``python3 InvenTree/manage.py runserver`` to launch a development server. This will launch the InvenTree web interface at ``127.0.0.1:8000``. For other options refer to the `django docs `_. +Run ``python3 InvenTree/manage.py runserver 127.0.0.1:8000`` to launch a development server. This will launch the InvenTree web interface at ``127.0.0.1:8000``. For other options refer to the `django docs `_. Database Migrations ------------------- @@ -54,6 +51,10 @@ Development and Testing Other shorthand functions are provided for the development and testing process: +* ``make requirements`` - Install all required underlying packages using PIP +* ``make secret`` - Generate the SECRET_KEY file for session validation +* ``make superuser`` - Create a superuser account +* ``make backup`` - Backup database tables and media files * ``make test`` - Run all unit tests * ``make coverage`` - Run all unit tests and generate code coverage report * ``make style`` - Check Python codebase against PEP coding standards (using Flake) From 0a328687a5a783a8b7e165c8f5223039045abf7d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 2 Aug 2019 11:11:28 +1000 Subject: [PATCH 08/13] BOM hash includes reference fields --- InvenTree/part/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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()) From e66fd956f970fea820a9bed97412288d1c3d2a9e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 2 Aug 2019 15:13:47 +1000 Subject: [PATCH 09/13] Fix available stock display --- InvenTree/part/templates/part/part_base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 @@ - + From 518fb78077fb086822bb6a65ef8ed1a48a88de10 Mon Sep 17 00:00:00 2001 From: Diego Herranz Date: Fri, 2 Aug 2019 06:42:44 +0100 Subject: [PATCH 10/13] Makefile: add .PHONY targets Given all the targets are actions instead of files to be generated, .PHONY should be used, mainly to avoid a conflict with a file of the same name. See https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html --- Makefile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 85acd5850c..dda44c9a03 100644 --- a/Makefile +++ b/Makefile @@ -45,4 +45,6 @@ documentation: backup: python3 InvenTree/manage.py dbbackup - python3 InvenTree/manage.py mediabackup \ No newline at end of file + python3 InvenTree/manage.py mediabackup + +.PHONY: clean migrate requirements secret superuser install style test coverage documentation backup \ No newline at end of file From b5f2c9d311e1347f79b4d8e40a323428543097f6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 2 Aug 2019 21:44:56 +1000 Subject: [PATCH 11/13] Add API endpoint for purchase orders - List and create purchase orders - List and create purchase order line items --- InvenTree/InvenTree/urls.py | 2 + InvenTree/order/api.py | 92 ++++++++++++++++++++++++++++++++++ InvenTree/order/serializers.py | 48 ++++++++++++++++++ 3 files changed, 142 insertions(+) create mode 100644 InvenTree/order/api.py create mode 100644 InvenTree/order/serializers.py diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index f88609bd81..11f4a4eda5 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -23,6 +23,7 @@ from part.api import part_api_urls, bom_api_urls from company.api import company_api_urls from stock.api import stock_api_urls from build.api import build_api_urls +from order.api import po_api_urls from django.conf import settings from django.conf.urls.static import static @@ -43,6 +44,7 @@ apipatterns = [ url(r'^company/', include(company_api_urls)), url(r'^stock/', include(stock_api_urls)), url(r'^build/', include(build_api_urls)), + url(r'^po/', include(po_api_urls)), # User URLs url(r'^user/', include(user_urls)), diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py new file mode 100644 index 0000000000..e1396cb54c --- /dev/null +++ b/InvenTree/order/api.py @@ -0,0 +1,92 @@ +""" +JSON API for the Order app +""" + +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import generics, permissions + +from django.conf.urls import url + +from .models import PurchaseOrder, PurchaseOrderLineItem +from .serializers import POSerializer, POLineItemSerializer + + +class POList(generics.ListCreateAPIView): + """ API endpoint for accessing a list of Order objects + + - GET: Return list of PO objects (with filters) + - POST: Create a new PurchaseOrder object + """ + + queryset = PurchaseOrder.objects.all() + serializer_class = POSerializer + + permission_classes = [ + permissions.IsAuthenticated, + ] + + filter_backends = [ + DjangoFilterBackend, + ] + + filter_fields = [ + 'supplier', + ] + + +class PODetail(generics.RetrieveUpdateAPIView): + """ API endpoint for detail view of a PurchaseOrder object """ + + queryset = PurchaseOrder.objects.all() + serializer_class = POSerializer + + permission_classes = [ + permissions.IsAuthenticated + ] + + +class POLineItemList(generics.ListCreateAPIView): + """ API endpoint for accessing a list of PO Line Item objects + + - GET: Return a list of PO Line Item objects + - POST: Create a new PurchaseOrderLineItem object + """ + + queryset = PurchaseOrderLineItem.objects.all() + serializer_class = POLineItemSerializer + + permission_classes = [ + permissions.IsAuthenticated, + ] + + filter_backends = [ + DjangoFilterBackend, + ] + + filter_fields = [ + 'order', + 'part' + ] + + +class POLineItemDetail(generics.RetrieveUpdateAPIView): + """ API endpoint for detail view of a PurchaseOrderLineItem object """ + + queryset = PurchaseOrderLineItem + serializer_class = POLineItemSerializer + + permission_classes = [ + permissions.IsAuthenticated, + ] + + +po_api_urls = [ + url(r'^order/(?P\d+)/?$', PODetail.as_view(), name='api-po-detail'), + url(r'^order/?$', POList.as_view(), name='api-po-list'), + + url(r'^line/(?P\d+)/?$', POLineItemDetail.as_view(), name='api-po-line-detail'), + url(r'^line/?$', POLineItemList.as_view(), name='api-po-line-list'), +] diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py new file mode 100644 index 0000000000..c4370015bc --- /dev/null +++ b/InvenTree/order/serializers.py @@ -0,0 +1,48 @@ +""" +JSON serializers for the Order API +""" + +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from InvenTree.serializers import InvenTreeModelSerializer + +from .models import PurchaseOrder, PurchaseOrderLineItem + + +class POSerializer(InvenTreeModelSerializer): + """ Serializes an Order object """ + + class Meta: + model = PurchaseOrder + + fields = [ + 'pk', + 'supplier', + 'reference', + 'description', + 'URL', + 'status', + 'notes', + ] + + read_only_fields = [ + 'reference', + 'status' + ] + + +class POLineItemSerializer(InvenTreeModelSerializer): + + class Meta: + model = PurchaseOrderLineItem + + fields = [ + 'pk', + 'quantity', + 'reference', + 'notes', + 'order', + 'part', + 'received', + ] From 9253ee2c9a1c774bef3ad3f08f02c78778c8dff5 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 2 Aug 2019 21:51:24 +1000 Subject: [PATCH 12/13] Fix the travis script to prevent requesting user input --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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: From 76ed23852a3c1922aab6ce33614ea0d79c468667 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 2 Aug 2019 21:51:24 +1000 Subject: [PATCH 13/13] Fix the travis script to prevent requesting user input (cherry picked from commit 9253ee2c9a1c774bef3ad3f08f02c78778c8dff5) --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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:
Part{{ item.part.full_name }} + {% include "hover_image.html" with image=item.part.image hover=True %} + {{ item.part.full_name }} +

Available Stock

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

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

In Stock