From 14e9b71ebb256d65b2e9f0521eff3bdf94a731cf Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 26 Apr 2019 18:51:37 +1000 Subject: [PATCH 01/19] Remove comment line in Makefile which doesn't play nice on Windows --- Makefile | 2 -- 1 file changed, 2 deletions(-) diff --git a/Makefile b/Makefile index 8db2a2110c..eee5122426 100644 --- a/Makefile +++ b/Makefile @@ -24,8 +24,6 @@ migrate: install: pip install -U -r requirements/base.txt - - # Generate a secret key python InvenTree/key.py setup: install migrate From 2076faaa6cdd6c4843db3f1fcf492dcb6e70b8c1 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 28 Apr 2019 11:49:20 +1000 Subject: [PATCH 02/19] Added docstring --- InvenTree/InvenTree/serializers.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index 7149668cc4..4ad5d1b87a 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -37,6 +37,11 @@ class InvenTreeModelSerializer(serializers.ModelSerializer): """ def validate(self, data): + """ Perform serializer validation. + In addition to running validators on the serializer fields, + this class ensures that the underlying model is also validated. + """ + # Run any native validation checks first (may throw an ValidationError) data = super(serializers.ModelSerializer, self).validate(data) From 4aef7643a06c5b3539ec7bf99e92625f0eef2372 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 4 May 2019 00:37:08 +1000 Subject: [PATCH 03/19] Fixed part image upload - Image was being saved twice - Don't call super().post in AjaxUpdateView - Instead, handle properly --- InvenTree/InvenTree/views.py | 9 ++++----- InvenTree/part/models.py | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 9a388c5b2c..711a4f685c 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -201,10 +201,8 @@ class AjaxUpdateView(AjaxMixin, UpdateView): """ super(UpdateView, self).get(request, *args, **kwargs) - - form = self.get_form() - return self.renderJsonResponse(request, form, context=self.get_context_data()) + return self.renderJsonResponse(request, self.get_form(), context=self.get_context_data()) def post(self, request, *args, **kwargs): """ Respond to POST request. @@ -215,7 +213,8 @@ class AjaxUpdateView(AjaxMixin, UpdateView): - Otherwise, return sucess status """ - super(UpdateView, self).post(request, *args, **kwargs) + # Make sure we have an object to point to + self.object = self.get_object() form = self.get_form() @@ -225,7 +224,7 @@ class AjaxUpdateView(AjaxMixin, UpdateView): if form.is_valid(): obj = form.save() - + # Include context data about the updated object data['pk'] = obj.id diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 7bce91c26c..9f5d9f0e29 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -115,10 +115,10 @@ class Part(models.Model): return reverse('part-detail', kwargs={'pk': self.id}) # Short name of the part - name = models.CharField(max_length=100, unique=True, help_text='Part name (must be unique)') + name = models.CharField(max_length=100, unique=True, blank=False, help_text='Part name (must be unique)') # Longer description of the part (optional) - description = models.CharField(max_length=250, help_text='Part description') + description = models.CharField(max_length=250, blank=False, help_text='Part description') # Internal Part Number (optional) # Potentially multiple parts map to the same internal IPN (variants?) From ff0d163a95ce4c9f7f960562a566880d1a960519 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 6 May 2019 18:05:29 +1000 Subject: [PATCH 04/19] Limit SupplierPart choices in EditStockItem view --- InvenTree/stock/views.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index fc174fa3f1..1f6f85ea1a 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -115,6 +115,22 @@ class StockItemEdit(AjaxUpdateView): ajax_template_name = 'modal_form.html' ajax_form_title = 'Edit Stock Item' + def get_form(self): + """ Get form for StockItem editing. + + Limit the choices for supplier_part + """ + + form = super(AjaxUpdateView, self).get_form() + + item = self.get_object() + + query = form.fields['supplier_part'].queryset + query = query.filter(part=item.part.id) + form.fields['supplier_part'].queryset = query + + return form + class StockLocationCreate(AjaxCreateView): """ From 080f9da9c8da48a00862222162f04661fcb31713 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 29 May 2019 22:05:13 +1000 Subject: [PATCH 05/19] Beginning to move the stocktake forms server side --- InvenTree/stock/forms.py | 21 ++++++++++++ .../stock/templates/stock/stock_move.html | 1 + InvenTree/stock/urls.py | 2 ++ InvenTree/stock/views.py | 33 +++++++++++++++++++ 4 files changed, 57 insertions(+) create mode 100644 InvenTree/stock/templates/stock/stock_move.html diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index a05c8caef0..6614d67d72 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -45,13 +45,34 @@ class CreateStockItemForm(HelperForm): class MoveStockItemForm(forms.ModelForm): """ Form for moving a StockItem to a new location """ + 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 + + location = forms.ChoiceField(label='Destination', required=True, help_text='Destination stock location') note = forms.CharField(label='Notes', required=True, help_text='Add note (required)') + transaction = forms.BooleanField(required=False, initial=False, label='Create Transaction', help_text='Create a stock transaction for these parts') + confirm = forms.BooleanField(required=False, initial=False, label='Confirm Stock Movement', help_text='Confirm movement of stock items') + + def __init__(self, *args, **kwargs): + super(MoveStockItemForm, self).__init__(*args, **kwargs) + + self.fields['location'].choices = self.get_location_choices() class Meta: model = StockItem fields = [ 'location', + 'note', + 'transaction', + 'confirm', ] diff --git a/InvenTree/stock/templates/stock/stock_move.html b/InvenTree/stock/templates/stock/stock_move.html new file mode 100644 index 0000000000..c7de8c74b2 --- /dev/null +++ b/InvenTree/stock/templates/stock/stock_move.html @@ -0,0 +1 @@ +{% extends "modal_form.html" %} \ No newline at end of file diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py index 37e54750de..513fff6cb0 100644 --- a/InvenTree/stock/urls.py +++ b/InvenTree/stock/urls.py @@ -36,6 +36,8 @@ stock_urls = [ url(r'^track/?', views.StockTrackingIndex.as_view(), name='stock-tracking-list'), + url(r'^move/', views.StockItemMoveMultiple.as_view(), name='stock-item-move-multiple'), + # Individual stock items url(r'^item/(?P\d+)/', include(stock_item_detail_urls)), diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 42ca16d3ba..4d0a2c8af2 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -5,10 +5,12 @@ Django views for interacting with Stock app # -*- coding: utf-8 -*- from __future__ import unicode_literals +from django.views.generic.edit import FormMixin from django.views.generic import DetailView, ListView from django.forms.models import model_to_dict from django.forms import HiddenInput +from InvenTree.views import AjaxView from InvenTree.views import AjaxUpdateView, AjaxDeleteView, AjaxCreateView from InvenTree.views import QRCodeView @@ -20,6 +22,7 @@ from .forms import CreateStockItemForm from .forms import EditStockItemForm from .forms import MoveStockItemForm from .forms import StocktakeForm +from .forms import MoveStockItemForm class StockIndex(ListView): @@ -120,6 +123,36 @@ class StockItemQRCode(QRCodeView): return item.format_barcode() except StockItem.DoesNotExist: return None + + +class StockItemMoveMultiple(AjaxView, FormMixin): + """ Move multiple stock items """ + + ajax_template_name = 'stock/stock_move.html' + ajax_form_title = 'Move Stock' + form_class = MoveStockItemForm + + + def get(self, request, *args, **kwargs): + + return self.renderJsonResponse(request, self.form_class()) + + def post(self, request, *args, **kwargs): + + form = self.get_form() + + valid = form.is_valid() + + print("Valid:", valid) + + data = { + 'form_valid': False, + } + + #form.errors['note'] = ['hello world'] + + return self.renderJsonResponse(request, form, data=data) + class StockItemEdit(AjaxUpdateView): From f4a052ee2b83ebb5d121d33f230574f6e9da24ba Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 29 May 2019 22:11:03 +1000 Subject: [PATCH 06/19] Redirect non-ajax forms to / --- InvenTree/InvenTree/views.py | 5 ++++- InvenTree/stock/urls.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index f6fdbc4d46..338e664252 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -9,7 +9,7 @@ as JSON objects and passing them to modal forms (using jQuery / bootstrap). from __future__ import unicode_literals from django.template.loader import render_to_string -from django.http import JsonResponse +from django.http import JsonResponse, HttpResponseRedirect from django.views import View from django.views.generic import UpdateView, CreateView @@ -132,6 +132,9 @@ class AjaxMixin(object): JSON response object """ + if not request.is_ajax(): + return HttpResponseRedirect('/') + if context is None: try: context = self.get_context_data() diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py index 513fff6cb0..237e2281f0 100644 --- a/InvenTree/stock/urls.py +++ b/InvenTree/stock/urls.py @@ -36,7 +36,7 @@ stock_urls = [ url(r'^track/?', views.StockTrackingIndex.as_view(), name='stock-tracking-list'), - url(r'^move/', views.StockItemMoveMultiple.as_view(), name='stock-item-move-multiple'), + url(r'^move/?', views.StockItemMoveMultiple.as_view(), name='stock-item-move-multiple'), # Individual stock items url(r'^item/(?P\d+)/', include(stock_item_detail_urls)), From 72dc75512b9f91b309226c5dd88f2a04ed1b9052 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 29 May 2019 22:23:45 +1000 Subject: [PATCH 07/19] Pass list of selected stock items to the view --- InvenTree/static/script/inventree/stock.js | 19 +++++++++++++++++++ InvenTree/stock/views.py | 18 ++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/InvenTree/static/script/inventree/stock.js b/InvenTree/static/script/inventree/stock.js index 800b23bbb6..4788f2c1df 100644 --- a/InvenTree/static/script/inventree/stock.js +++ b/InvenTree/static/script/inventree/stock.js @@ -550,6 +550,24 @@ function loadStockTable(table, options) { $("#multi-item-move").click(function() { + var items = $('#stock-table').bootstrapTable('getSelections'); + + var stock = []; + + items.forEach(function(item) { + stock.push(item.pk); + }); + + launchModalForm("/stock/move/", + { + data: { + stock: stock, + }, + } + ); + + /* + var items = $("#stock-table").bootstrapTable('getSelections'); moveStockItems(items, @@ -560,6 +578,7 @@ function loadStockTable(table, options) { }); return false; + */ }); } diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 4d0a2c8af2..990287202d 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -132,9 +132,27 @@ class StockItemMoveMultiple(AjaxView, FormMixin): ajax_form_title = 'Move Stock' form_class = MoveStockItemForm + def get_items(self, item_list): + """ Return list of stock items. """ + + items = [] + + for pk in item_list: + try: + items.append(StockItem.objects.get(pk=pk)) + except StockItem.DoesNotExist: + pass + + return items def get(self, request, *args, **kwargs): + item_list = request.GET.getlist('stock[]') + + items = self.get_items(item_list) + + print(items) + return self.renderJsonResponse(request, self.form_class()) def post(self, request, *args, **kwargs): From c7877c67ff89f506c6cb3b76d53caa2496881cb8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 24 Jun 2019 23:51:46 +1000 Subject: [PATCH 08/19] Add a simple endpoint with server information --- InvenTree/InvenTree/urls.py | 4 ++++ InvenTree/InvenTree/views.py | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 716ff25767..f88609bd81 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -31,6 +31,7 @@ from django.views.generic.base import RedirectView from rest_framework.documentation import include_docs_urls from .views import IndexView, SearchView, SettingsView, EditUserView, SetPasswordView +from .views import InfoView from users.urls import user_urls @@ -45,6 +46,9 @@ apipatterns = [ # User URLs url(r'^user/', include(user_urls)), + + # InvenTree information endpoint + url(r'^$', InfoView.as_view(), name='inventree-info'), ] urlpatterns = [ diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index a564705ac2..610f3c0c00 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -19,6 +19,7 @@ from part.models import Part from .forms import DeleteForm, EditUserForm, SetPasswordForm from .helpers import str2bool +from .version import inventreeVersion from rest_framework import views @@ -392,6 +393,21 @@ class AjaxDeleteView(AjaxMixin, UpdateView): return self.renderJsonResponse(request, form, data=data, context=context) +class InfoView(AjaxView): + """ Simple JSON endpoint for InvenTree information. + Use to confirm that the server is running, etc. + """ + + def get(self, request, *args, **kwargs): + + data = { + 'server': 'InvenTree', + 'version': inventreeVersion() + } + + return JsonResponse(data) + + class EditUserView(AjaxUpdateView): """ View for editing user information """ From b23017b6bf9c0796362df988bb49fd30d9d86fcf Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 9 Jun 2020 13:14:29 +1000 Subject: [PATCH 09/19] Fix display of buttons for stock item --- InvenTree/stock/templates/stock/item_base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index 657154c446..331b031e91 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -85,12 +85,12 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% endif %} + {% endif %} {% if item.part.salable %} {% endif %} - {% endif %} From f7ed48809cb0d73f21b488988f067dcecdc33099 Mon Sep 17 00:00:00 2001 From: Ben Charlton Date: Fri, 21 Aug 2020 17:36:49 +0100 Subject: [PATCH 10/19] Support non-integer serial numbers --- InvenTree/InvenTree/helpers.py | 12 ++++-------- InvenTree/part/models.py | 4 +++- .../migrations/0050_auto_20200821_1403.py | 18 ++++++++++++++++++ InvenTree/stock/models.py | 7 ++----- InvenTree/stock/serializers.py | 2 +- 5 files changed, 28 insertions(+), 15 deletions(-) create mode 100644 InvenTree/stock/migrations/0050_auto_20200821_1403.py diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index 28cebbcd3d..4ec84c7912 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -371,14 +371,10 @@ def ExtractSerialNumbers(serials, expected_quantity): continue else: - try: - n = int(group) - if n in numbers: - errors.append(_("Duplicate serial: {n}".format(n=n))) - else: - numbers.append(n) - except ValueError: - errors.append(_("Invalid group: {g}".format(g=group))) + if group in numbers: + errors.append(_("Duplicate serial: {g}".format(g=group))) + else: + numbers.append(group) if len(errors) > 0: raise ValidationError(errors) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index dd126d5730..6d42b08101 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -346,8 +346,10 @@ class Part(MPTTModel): if n is None: return 1 - else: + elif n is int: return n + 1 + else: + return None def getSerialNumberString(self, quantity): """ diff --git a/InvenTree/stock/migrations/0050_auto_20200821_1403.py b/InvenTree/stock/migrations/0050_auto_20200821_1403.py new file mode 100644 index 0000000000..fa02c0d0f7 --- /dev/null +++ b/InvenTree/stock/migrations/0050_auto_20200821_1403.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.7 on 2020-08-21 14:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0049_auto_20200820_0454'), + ] + + operations = [ + migrations.AlterField( + model_name='stockitem', + name='serial', + field=models.CharField(blank=True, help_text='Serial number for this item', max_length=100, null=True, verbose_name='Serial Number'), + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 49c185220f..c10295ee6a 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -355,9 +355,9 @@ class StockItem(MPTTModel): verbose_name=_("Customer"), ) - serial = models.PositiveIntegerField( + serial = models.CharField( verbose_name=_('Serial Number'), - blank=True, null=True, + max_length=100, blank=True, null=True, help_text=_('Serial number for this item') ) @@ -687,9 +687,6 @@ class StockItem(MPTTModel): 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")}) diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index bed3f8f7c1..2967538e88 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -129,7 +129,7 @@ class StockItemSerializer(InvenTreeModelSerializer): allocated = serializers.FloatField(source='allocation_count', required=False) - serial = serializers.IntegerField(required=False) + serial = serializers.CharField(required=False) required_tests = serializers.IntegerField(source='required_test_count', read_only=True, required=False) From c31b30bf83c27bb4c2ac212bfff00fac4c30b833 Mon Sep 17 00:00:00 2001 From: Ben Charlton Date: Fri, 21 Aug 2020 18:53:51 +0100 Subject: [PATCH 11/19] Fix simple tests --- InvenTree/build/test_build.py | 2 +- InvenTree/part/models.py | 8 +++++--- InvenTree/stock/tests.py | 5 +---- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/InvenTree/build/test_build.py b/InvenTree/build/test_build.py index 32ad33dab3..bb7931d2f1 100644 --- a/InvenTree/build/test_build.py +++ b/InvenTree/build/test_build.py @@ -220,5 +220,5 @@ class BuildTest(TestCase): # And a new stock item created for the build output self.assertEqual(StockItem.objects.get(pk=7).quantity, 1) - self.assertEqual(StockItem.objects.get(pk=7).serial, 1) + self.assertEqual(StockItem.objects.get(pk=7).serial, "1") self.assertEqual(StockItem.objects.get(pk=7).build, self.build) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 6d42b08101..cea78543c6 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -346,10 +346,12 @@ class Part(MPTTModel): if n is None: return 1 - elif n is int: - return n + 1 else: - return None + try: + return int(n) + 1 + except ValueError: + return None + def getSerialNumberString(self, quantity): """ diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py index 513368c422..03a04b73d8 100644 --- a/InvenTree/stock/tests.py +++ b/InvenTree/stock/tests.py @@ -295,10 +295,7 @@ class StockTest(TestCase): 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) - + # Not enough serial numbers for all stock items. with self.assertRaises(ValidationError): item.serializeStock(3, "hello", self.user) From 23cc3d9b060e6651cf2b8dc385838989777b8760 Mon Sep 17 00:00:00 2001 From: Ben Charlton Date: Fri, 21 Aug 2020 19:17:58 +0100 Subject: [PATCH 12/19] Handle 'next serial' more gracefully --- InvenTree/part/models.py | 5 ++++- InvenTree/stock/tests.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index cea78543c6..fba217e0d9 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -14,6 +14,8 @@ from django.urls import reverse from django.db import models, transaction from django.db.models import Sum from django.db.models.functions import Coalesce +from django.db.models import IntegerField +from django.db.models.functions import Cast from django.core.validators import MinValueValidator from django.contrib.auth.models import User @@ -329,7 +331,8 @@ class Part(MPTTModel): """ parts = Part.objects.filter(tree_id=self.tree_id) - stock = StockModels.StockItem.objects.filter(part__in=parts).exclude(serial=None).order_by('-serial') + stock = StockModels.StockItem.objects.filter(part__in=parts).exclude(serial=None).annotate( + serial_as_int=Cast('serial', output_field=IntegerField())).order_by('-serial_as_int') if stock.count() > 0: return stock.first().serial diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py index 03a04b73d8..5442584b7d 100644 --- a/InvenTree/stock/tests.py +++ b/InvenTree/stock/tests.py @@ -391,8 +391,8 @@ class VariantTest(StockTest): with self.assertRaises(ValidationError): item.save() - # This should pass - item.serial = n + 1 + # This should pass, although not strictly an int field now. + item.serial = int(n) + 1 item.save() # Attempt to create the same serial number but for a variant (should fail!) From d5a374f1fda3e738f706913bc61bcb21eaca5661 Mon Sep 17 00:00:00 2001 From: Ben Charlton Date: Mon, 24 Aug 2020 19:49:32 +0100 Subject: [PATCH 13/19] Make serial number suggestion DB independent, handle mixed types more cleanly and test --- InvenTree/part/models.py | 26 +++++++++++++++----------- InvenTree/stock/tests.py | 5 +++++ 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index b8e9c859bb..8a62746bd5 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -330,14 +330,21 @@ class Part(MPTTModel): """ parts = Part.objects.filter(tree_id=self.tree_id) - stock = StockModels.StockItem.objects.filter(part__in=parts).exclude(serial=None).annotate( - serial_as_int=Cast('serial', output_field=IntegerField())).order_by('-serial_as_int') - - if stock.count() > 0: - return stock.first().serial + stock = StockModels.StockItem.objects.filter(part__in=parts).exclude(serial=None) + try: + ordered = sorted(stock.all(), reverse=True, key=lambda n: int(n.serial)) + + if len(ordered) > 0: + return ordered[0].serial + + # Non-numeric serials, so don't suggest one. + except ValueError: + return None + # No serial numbers found - return None + return 0 + def getNextSerialNumber(self): """ @@ -347,12 +354,9 @@ class Part(MPTTModel): n = self.getHighestSerialNumber() if n is None: - return 1 + return None else: - try: - return int(n) + 1 - except ValueError: - return None + return int(n) + 1 def getSerialNumberString(self, quantity): diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py index 5442584b7d..df37153559 100644 --- a/InvenTree/stock/tests.py +++ b/InvenTree/stock/tests.py @@ -391,6 +391,11 @@ class VariantTest(StockTest): with self.assertRaises(ValidationError): item.save() + # Verify items with a non-numeric serial don't offer a next serial. + item.serial="string" + item.save() + self.assertEqual(variant.getNextSerialNumber(), None) + # This should pass, although not strictly an int field now. item.serial = int(n) + 1 item.save() From 0da2682c689ae9642ceef527023c897fdef787b4 Mon Sep 17 00:00:00 2001 From: Ben Charlton Date: Mon, 24 Aug 2020 20:00:19 +0100 Subject: [PATCH 14/19] handle non-int serial range suggestions cleanly --- InvenTree/part/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 8a62746bd5..5a3fd229b4 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -367,6 +367,9 @@ class Part(MPTTModel): sn = self.getNextSerialNumber() + if sn is None: + return None + if quantity >= 2: sn = "{n}-{m}".format( n=sn, From 9c2d13b487825ad6734e49a3dbc2ba7e347354da Mon Sep 17 00:00:00 2001 From: Ben Charlton Date: Mon, 24 Aug 2020 20:04:10 +0100 Subject: [PATCH 15/19] test non-numeric serials are handled correctly for ranges --- InvenTree/stock/tests.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py index df37153559..5f4e140ebd 100644 --- a/InvenTree/stock/tests.py +++ b/InvenTree/stock/tests.py @@ -396,6 +396,9 @@ class VariantTest(StockTest): item.save() self.assertEqual(variant.getNextSerialNumber(), None) + # And the same for the range when serializing. + self.assertEqual(variant.getSerialNumberString(5), None) + # This should pass, although not strictly an int field now. item.serial = int(n) + 1 item.save() From e0a744b01dc991a47310d492e8f70c0f04e465c5 Mon Sep 17 00:00:00 2001 From: Ben Charlton Date: Mon, 24 Aug 2020 20:09:06 +0100 Subject: [PATCH 16/19] Remove no-longer-needed cast functions --- InvenTree/part/models.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 5a3fd229b4..49dd572d8c 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -14,8 +14,6 @@ from django.urls import reverse from django.db import models, transaction from django.db.models import Sum from django.db.models.functions import Coalesce -from django.db.models import IntegerField -from django.db.models.functions import Cast from django.core.validators import MinValueValidator from django.contrib.auth.models import User From 06552832cb6e9323f2cf0951c19205b526771e33 Mon Sep 17 00:00:00 2001 From: Ben Charlton Date: Fri, 28 Aug 2020 17:30:16 +0100 Subject: [PATCH 17/19] Style corrections --- InvenTree/part/models.py | 2 -- InvenTree/stock/tests.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 49cd26b039..0bf3e0bb35 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -343,7 +343,6 @@ class Part(MPTTModel): # No serial numbers found return 0 - def getNextSerialNumber(self): """ Return the next-available serial number for this Part. @@ -356,7 +355,6 @@ class Part(MPTTModel): else: return int(n) + 1 - def getSerialNumberString(self, quantity): """ Return a formatted string representing the next available serial numbers, diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py index 5f4e140ebd..bac7e735f8 100644 --- a/InvenTree/stock/tests.py +++ b/InvenTree/stock/tests.py @@ -392,7 +392,7 @@ class VariantTest(StockTest): item.save() # Verify items with a non-numeric serial don't offer a next serial. - item.serial="string" + item.serial = "string" item.save() self.assertEqual(variant.getNextSerialNumber(), None) From 74f25ee7f9b7138130c5b655f460690dc1277b72 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 1 Sep 2020 20:16:46 +1000 Subject: [PATCH 18/19] Part: Extend logic for "latest" serial number - Return value if it is a string - Remove 'getNextSerialNumber' function - Improve functionality of function which returns serial number placeholder string --- InvenTree/build/views.py | 6 +- InvenTree/part/models.py | 69 ++++++++++++++--------- InvenTree/part/templates/part/detail.html | 10 +++- InvenTree/stock/tests.py | 10 ++-- InvenTree/stock/views.py | 10 +--- 5 files changed, 57 insertions(+), 48 deletions(-) diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 2124207c7d..88dc66085f 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -197,12 +197,8 @@ class BuildComplete(AjaxUpdateView): if not build.part.trackable: form.fields.pop('serial_numbers') else: - if build.quantity == 1: - text = _('Next available serial number is') - else: - text = _('Next available serial numbers are') - form.field_placeholder['serial_numbers'] = text + " " + build.part.getSerialNumberString(build.quantity) + form.field_placeholder['serial_numbers'] = build.part.getSerialNumberString(build.quantity) form.rebuild_layout() diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 0d19a0afb9..6190c9b4f0 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -319,9 +319,12 @@ class Part(MPTTModel): return stock.exists() - def getHighestSerialNumber(self): + def getLatestSerialNumber(self): """ - Return the highest serial number for this Part. + Return the "latest" serial number for this Part. + + If *all* the serial numbers are integers, then this will return the highest one. + Otherwise, it will simply return the serial number most recently added. Note: Serial numbers must be unique across an entire Part "tree", so we filter by the entire tree. @@ -330,51 +333,61 @@ class Part(MPTTModel): parts = Part.objects.filter(tree_id=self.tree_id) stock = StockModels.StockItem.objects.filter(part__in=parts).exclude(serial=None) + # There are no matchin StockItem objects (skip further tests) + if not stock.exists(): + return None + + # Attempt to coerce the returned serial numbers to integers + # If *any* are not integers, fail! try: ordered = sorted(stock.all(), reverse=True, key=lambda n: int(n.serial)) if len(ordered) > 0: return ordered[0].serial - # Non-numeric serials, so don't suggest one. + # One or more of the serial numbers was non-numeric + # In this case, the "best" we can do is return the most recent except ValueError: - return None + return stock.last().serial # No serial numbers found - return 0 + return None - def getNextSerialNumber(self): - """ - Return the next-available serial number for this Part. - """ - - n = self.getHighestSerialNumber() - - if n is None: - return None - else: - return int(n) + 1 - - def getSerialNumberString(self, quantity): + def getSerialNumberString(self, quantity=1): """ Return a formatted string representing the next available serial numbers, given a certain quantity of items. """ - sn = self.getNextSerialNumber() + latest = self.getLatestSerialNumber() - if sn is None: - return None + quantity = int(quantity) + + # No serial numbers can be found, assume 1 as the first serial + if latest is None: + latest = 0 + + # Attempt to turn into an integer + try: + latest = int(latest) + except: + pass + + if type(latest) is int: + + if quantity >= 2: + text = '{n} - {m}'.format(n=latest+1, m=latest+1+quantity) + + return _('Next available serial numbers are') + ' ' + text + else: + text = str(latest) + + return _('Next available serial number is') + ' ' + text - if quantity >= 2: - sn = "{n}-{m}".format( - n=sn, - m=int(sn + quantity - 1) - ) else: - sn = str(sn) + # Non-integer values, no option but to return latest - return sn + return _('Most recent serial number is') + ' ' + str(latest) @property def full_name(self): diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index 18970fe373..eee68c6d6d 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -36,8 +36,14 @@ {% if part.trackable %} - {% trans "Next Serial Number" %} - {{ part.getNextSerialNumber }} + {% trans "Latest Serial Number" %} + + {% if part.getLatestSerialNumber %} + {{ part.getLatestSerialNumber }} + {% else %} + {% trans "No serial numbers recorded" %} + {% endif %} + {% endif %} diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py index bac7e735f8..1001ed12ea 100644 --- a/InvenTree/stock/tests.py +++ b/InvenTree/stock/tests.py @@ -372,14 +372,14 @@ class VariantTest(StockTest): self.assertFalse(chair.checkIfSerialNumberExists(30)) - self.assertEqual(chair.getNextSerialNumber(), 23) + self.assertEqual(chair.getLatestSerialNumber(), '22') # Same operations on a sub-item variant = Part.objects.get(pk=10003) - self.assertEqual(variant.getNextSerialNumber(), 23) + self.assertEqual(variant.getLatestSerialNumber(), '22') # Create a new serial number - n = variant.getHighestSerialNumber() + n = variant.getLatestSerialNumber() item = StockItem( part=variant, @@ -394,10 +394,8 @@ class VariantTest(StockTest): # Verify items with a non-numeric serial don't offer a next serial. item.serial = "string" item.save() - self.assertEqual(variant.getNextSerialNumber(), None) - # And the same for the range when serializing. - self.assertEqual(variant.getSerialNumberString(5), None) + self.assertEqual(variant.getLatestSerialNumber(), "string") # This should pass, although not strictly an int field now. item.serial = int(n) + 1 diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index acbc9c0863..2833a47af9 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -1234,8 +1234,9 @@ class StockItemCreate(AjaxCreateView): part = self.get_part(form=form) if part is not None: - sn = part.getNextSerialNumber() - form.field_placeholder['serial_numbers'] = _('Next available serial number is') + ' ' + str(sn) + + # Add placeholder text for the serial number field + form.field_placeholder['serial_numbers'] = part.getSerialNumberString() form.rebuild_layout() @@ -1353,11 +1354,6 @@ class StockItemCreate(AjaxCreateView): part = Part.objects.get(id=part_id) quantity = Decimal(form['quantity'].value()) - sn = part.getNextSerialNumber() - form.field_placeholder['serial_numbers'] = _("Next available serial number is") + " " + str(sn) - - form.rebuild_layout() - except (Part.DoesNotExist, ValueError, InvalidOperation): part = None quantity = 1 From 4613730a1933466f470b96949752190371050f51 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 1 Sep 2020 20:24:14 +1000 Subject: [PATCH 19/19] PEP fixes --- InvenTree/part/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 6190c9b4f0..fea2f56f12 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -376,7 +376,7 @@ class Part(MPTTModel): if type(latest) is int: if quantity >= 2: - text = '{n} - {m}'.format(n=latest+1, m=latest+1+quantity) + text = '{n} - {m}'.format(n=latest + 1, m=latest + 1 + quantity) return _('Next available serial numbers are') + ' ' + text else: