diff --git a/.gitignore b/.gitignore index 420524d06f..74669a5756 100644 --- a/.gitignore +++ b/.gitignore @@ -78,5 +78,4 @@ locale_stats.json # node.js package-lock.json -package.json node_modules/ \ No newline at end of file diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index 0448a69781..b42d54cbe9 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -49,6 +49,9 @@ class ReferenceIndexingMixin(models.Model): """ A mixin for keeping track of numerical copies of the "reference" field. + !!DANGER!! always add `ReferenceIndexingSerializerMixin`to all your models serializers to + ensure the reference field is not too big + Here, we attempt to convert a "reference" field value (char) to an integer, for performing fast natural sorting. @@ -69,22 +72,25 @@ class ReferenceIndexingMixin(models.Model): reference = getattr(self, 'reference', '') - # Default value if we cannot convert to an integer - ref_int = 0 + self.reference_int = extract_int(reference) - # Look at the start of the string - can it be "integerized"? - result = re.match(r"^(\d+)", reference) + reference_int = models.BigIntegerField(default=0) - if result and len(result.groups()) == 1: - ref = result.groups()[0] - try: - ref_int = int(ref) - except: - ref_int = 0 - self.reference_int = ref_int +def extract_int(reference): + # Default value if we cannot convert to an integer + ref_int = 0 - reference_int = models.IntegerField(default=0) + # Look at the start of the string - can it be "integerized"? + result = re.match(r"^(\d+)", reference) + + if result and len(result.groups()) == 1: + ref = result.groups()[0] + try: + ref_int = int(ref) + except: + ref_int = 0 + return ref_int class InvenTreeAttachment(models.Model): diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index 3785cfb292..59ba0295cb 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -16,6 +16,7 @@ from django.conf import settings from django.contrib.auth.models import User from django.core.exceptions import ValidationError as DjangoValidationError from django.utils.translation import ugettext_lazy as _ +from django.db import models from djmoney.contrib.django_rest_framework.fields import MoneyField from djmoney.money import Money @@ -27,6 +28,8 @@ from rest_framework.fields import empty from rest_framework.exceptions import ValidationError from rest_framework.serializers import DecimalField +from .models import extract_int + class InvenTreeMoneySerializer(MoneyField): """ @@ -239,6 +242,17 @@ class InvenTreeModelSerializer(serializers.ModelSerializer): return data +class ReferenceIndexingSerializerMixin(): + """ + This serializer mixin ensures the the reference is not to big / small + for the BigIntegerField + """ + def validate_reference(self, value): + if extract_int(value) > models.BigIntegerField.MAX_BIGINT: + raise serializers.ValidationError('reference is to to big') + return value + + class InvenTreeAttachmentSerializerField(serializers.FileField): """ Override the DRF native FileField serializer, diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index cd9ce410de..a61696f547 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -781,6 +781,7 @@ input[type="submit"] { .btn-small { padding: 3px; padding-left: 5px; + padding-right: 5px; } .btn-remove { diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py index e0e64f5525..bbf0174453 100644 --- a/InvenTree/InvenTree/version.py +++ b/InvenTree/InvenTree/version.py @@ -12,11 +12,15 @@ import common.models INVENTREE_SW_VERSION = "0.6.0 dev" # InvenTree API version -INVENTREE_API_VERSION = 18 +INVENTREE_API_VERSION = 19 """ Increment this API version number whenever there is a significant change to the API that any clients need to know about +v19 -> 2021-12-02 + - Adds the ability to filter the StockItem API by "part_tree" + - Returns only stock items which match a particular part.tree_id field + v18 -> 2021-11-15 - Adds the ability to filter BomItem API by "uses" field - This returns a list of all BomItems which "use" the specified part diff --git a/InvenTree/build/migrations/0034_alter_build_reference_int.py b/InvenTree/build/migrations/0034_alter_build_reference_int.py new file mode 100644 index 0000000000..8b14d3812e --- /dev/null +++ b/InvenTree/build/migrations/0034_alter_build_reference_int.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.5 on 2021-12-01 21:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('build', '0033_auto_20211128_0151'), + ] + + operations = [ + migrations.AlterField( + model_name='build', + name='reference_int', + field=models.BigIntegerField(default=0), + ), + ] diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 35a3bb3baa..2431855d3c 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -16,7 +16,7 @@ from rest_framework import serializers from rest_framework.serializers import ValidationError from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentSerializer -from InvenTree.serializers import UserSerializerBrief +from InvenTree.serializers import UserSerializerBrief, ReferenceIndexingSerializerMixin import InvenTree.helpers from InvenTree.serializers import InvenTreeDecimalField @@ -32,7 +32,7 @@ from users.serializers import OwnerSerializer from .models import Build, BuildItem, BuildOrderAttachment -class BuildSerializer(InvenTreeModelSerializer): +class BuildSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer): """ Serializes a Build object """ diff --git a/InvenTree/order/migrations/0054_auto_20211201_2139.py b/InvenTree/order/migrations/0054_auto_20211201_2139.py new file mode 100644 index 0000000000..8a66bbea12 --- /dev/null +++ b/InvenTree/order/migrations/0054_auto_20211201_2139.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.5 on 2021-12-01 21:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0053_auto_20211128_0151'), + ] + + operations = [ + migrations.AlterField( + model_name='purchaseorder', + name='reference_int', + field=models.BigIntegerField(default=0), + ), + migrations.AlterField( + model_name='salesorder', + name='reference_int', + field=models.BigIntegerField(default=0), + ), + ] diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 0528d57596..38a058ae6d 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -24,6 +24,7 @@ from InvenTree.serializers import InvenTreeAttachmentSerializer from InvenTree.serializers import InvenTreeModelSerializer from InvenTree.serializers import InvenTreeDecimalField from InvenTree.serializers import InvenTreeMoneySerializer +from InvenTree.serializers import ReferenceIndexingSerializerMixin from InvenTree.status_codes import StockStatus from part.serializers import PartBriefSerializer @@ -39,7 +40,7 @@ from .models import SalesOrderAllocation from users.serializers import OwnerSerializer -class POSerializer(InvenTreeModelSerializer): +class POSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer): """ Serializer for a PurchaseOrder object """ def __init__(self, *args, **kwargs): @@ -394,7 +395,7 @@ class POAttachmentSerializer(InvenTreeAttachmentSerializer): ] -class SalesOrderSerializer(InvenTreeModelSerializer): +class SalesOrderSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer): """ Serializers for the SalesOrder object """ diff --git a/InvenTree/order/test_api.py b/InvenTree/order/test_api.py index 94e9696e0a..44effe9fb6 100644 --- a/InvenTree/order/test_api.py +++ b/InvenTree/order/test_api.py @@ -105,6 +105,25 @@ class PurchaseOrderTest(OrderTest): self.assertEqual(data['pk'], 1) self.assertEqual(data['description'], 'Ordering some screws') + def test_po_reference(self): + """test that a reference with a too big / small reference is not possible""" + # get permissions + self.assignRole('purchase_order.add') + + url = reverse('api-po-list') + huge_numer = 9223372036854775808 + + # too big + self.post( + url, + { + 'supplier': 1, + 'reference': huge_numer, + 'description': 'PO not created via the API', + }, + expected_code=400 + ) + def test_po_attachments(self): url = reverse('api-po-attachment-list') diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 403934d3d9..dbc1140214 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -1075,6 +1075,7 @@ class PartList(generics.ListCreateAPIView): 'revision', 'keywords', 'category__name', + 'manufacturer_parts__MPN', ] diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index 994eefe94e..d2505a57f7 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -322,7 +322,14 @@ {% trans "Latest Serial Number" %} - {{ part.getLatestSerialNumber }}{% include "clip.html"%} + + {{ part.getLatestSerialNumber }} +
+ + + +
+ {% endif %} {% if part.default_location %} @@ -577,4 +584,8 @@ $('#collapse-part-details').collapse('show'); } + $('#serial-number-search').click(function() { + findStockItemBySerialNumber({{ part.pk }}); + }); + {% endblock %} \ No newline at end of file diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index fcfa58d01a..8385041209 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -313,7 +313,7 @@ class StockFilter(rest_filters.FilterSet): # Serial number filtering serial_gte = rest_filters.NumberFilter(label='Serial number GTE', field_name='serial', lookup_expr='gte') serial_lte = rest_filters.NumberFilter(label='Serial number LTE', field_name='serial', lookup_expr='lte') - serial = rest_filters.NumberFilter(label='Serial number', field_name='serial', lookup_expr='exact') + serial = rest_filters.CharFilter(label='Serial number', field_name='serial', lookup_expr='exact') serialized = rest_filters.BooleanFilter(label='Has serial number', method='filter_serialized') @@ -703,6 +703,18 @@ class StockList(generics.ListCreateAPIView): except (ValueError, StockItem.DoesNotExist): pass + # Filter by "part tree" - only allow parts within a given variant tree + part_tree = params.get('part_tree', None) + + if part_tree is not None: + try: + part = Part.objects.get(pk=part_tree) + + if part.tree_id is not None: + queryset = queryset.filter(part__tree_id=part.tree_id) + except: + pass + # Filter by 'allocated' parts? allocated = params.get('allocated', None) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 8457ca39ab..0aa63687c9 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -7,7 +7,6 @@ Stock database model definitions from __future__ import unicode_literals import os -import re from django.utils.translation import gettext_lazy as _ from django.core.exceptions import ValidationError, FieldError @@ -39,6 +38,7 @@ import label.models from InvenTree.status_codes import StockStatus, StockHistoryCode from InvenTree.models import InvenTreeTree, InvenTreeAttachment from InvenTree.fields import InvenTreeModelMoneyField, InvenTreeURLField +from InvenTree.serializers import extract_int from users.models import Owner @@ -236,17 +236,7 @@ class StockItem(MPTTModel): serial_int = 0 if serial is not None: - - serial = str(serial) - - # Look at the start of the string - can it be "integerized"? - result = re.match(r'^(\d+)', serial) - - if result and len(result.groups()) == 1: - try: - serial_int = int(result.groups()[0]) - except: - serial_int = 0 + serial_int = extract_int(str(serial)) self.serial_int = serial_int diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index c74d674275..9bd5ea64be 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -32,7 +32,7 @@ from company.serializers import SupplierPartSerializer import InvenTree.helpers import InvenTree.serializers -from InvenTree.serializers import InvenTreeDecimalField +from InvenTree.serializers import InvenTreeDecimalField, extract_int from part.serializers import PartBriefSerializer @@ -73,6 +73,11 @@ class StockItemSerializerBrief(InvenTree.serializers.InvenTreeModelSerializer): 'uid', ] + def validate_serial(self, value): + if extract_int(value) > 2147483647: + raise serializers.ValidationError('serial is to to big') + return value + class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer): """ Serializer for a StockItem: diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index 5f22076d9a..0d5ab272d6 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -148,17 +148,24 @@ {% trans "Serial Number" %} - {% if previous %} - - {{ previous.serial }} ‹ - - {% endif %} - {{ item.serial }} - {% if next %} - - › {{ next.serial }} - - {% endif %} + {{ item.serial }} +
+ {% if previous %} + + + {{ previous.serial }} + + {% endif %} + + + + {% if next %} + + {{ next.serial }} + + + {% endif %} +
{% else %} @@ -592,4 +599,8 @@ $("#stock-return-from-customer").click(function() { {% endif %} +$('#serial-number-search').click(function() { + findStockItemBySerialNumber({{ item.part.pk }}); +}); + {% endblock %} diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index 448d06b23f..5cd09f854e 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -121,7 +121,6 @@ {% include 'modals.html' %} {% include 'about.html' %} - {% include "notifications.html" %} diff --git a/InvenTree/templates/js/translated/stock.js b/InvenTree/templates/js/translated/stock.js index 5e92299f03..02ea3c2c3b 100644 --- a/InvenTree/templates/js/translated/stock.js +++ b/InvenTree/templates/js/translated/stock.js @@ -44,6 +44,7 @@ editStockItem, editStockLocation, exportStock, + findStockItemBySerialNumber, loadInstalledInTable, loadStockLocationTable, loadStockTable, @@ -394,6 +395,87 @@ function createNewStockItem(options={}) { constructForm(url, options); } +/* + * Launch a modal form to find a particular stock item by serial number. + * Arguments: + * - part: ID (PK) of the part in question + */ + +function findStockItemBySerialNumber(part_id) { + + constructFormBody({}, { + title: '{% trans "Find Serial Number" %}', + fields: { + serial: { + label: '{% trans "Serial Number" %}', + help_text: '{% trans "Enter serial number" %}', + placeholder: '{% trans "Enter serial number" %}', + required: true, + type: 'string', + value: '', + } + }, + onSubmit: function(fields, opts) { + + var serial = getFormFieldValue('serial', fields['serial'], opts); + + serial = serial.toString().trim(); + + if (!serial) { + handleFormErrors( + { + 'serial': [ + '{% trans "Enter a serial number" %}', + ] + }, fields, opts + ); + return; + } + + inventreeGet( + '{% url "api-stock-list" %}', + { + part_tree: part_id, + serial: serial, + }, + { + success: function(response) { + if (response.length == 0) { + // No results! + handleFormErrors( + { + 'serial': [ + '{% trans "No matching serial number" %}', + ] + }, fields, opts + ); + } else if (response.length > 1) { + // Too many results! + handleFormErrors( + { + 'serial': [ + '{% trans "More than one matching result found" %}', + ] + }, fields, opts + ); + } else { + $(opts.modal).modal('hide'); + + // Redirect + var pk = response[0].pk; + location.href = `/stock/item/${pk}/`; + } + }, + error: function(xhr) { + showApiError(xhr, opts.url); + $(opts.modal).modal('hide'); + } + } + ); + } + }); +} + /* Stock API functions * Requires api.js to be loaded first diff --git a/package.json b/package.json new file mode 100644 index 0000000000..b4f14ec92f --- /dev/null +++ b/package.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "eslint": "^8.3.0", + "eslint-config-google": "^0.14.0", + "markuplint": "^1.11.4" + } +}