2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-29 12:06:44 +00:00

Merge branch 'master' of https://github.com/inventree/InvenTree into workflow-remaster

This commit is contained in:
Matthias 2021-12-02 12:58:58 +01:00
commit f71ac93668
No known key found for this signature in database
GPG Key ID: F50EF5741D33E076
19 changed files with 248 additions and 45 deletions

1
.gitignore vendored
View File

@ -78,5 +78,4 @@ locale_stats.json
# node.js # node.js
package-lock.json package-lock.json
package.json
node_modules/ node_modules/

View File

@ -49,6 +49,9 @@ class ReferenceIndexingMixin(models.Model):
""" """
A mixin for keeping track of numerical copies of the "reference" field. 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, Here, we attempt to convert a "reference" field value (char) to an integer,
for performing fast natural sorting. for performing fast natural sorting.
@ -69,22 +72,25 @@ class ReferenceIndexingMixin(models.Model):
reference = getattr(self, 'reference', '') reference = getattr(self, 'reference', '')
# Default value if we cannot convert to an integer self.reference_int = extract_int(reference)
ref_int = 0
# Look at the start of the string - can it be "integerized"? reference_int = models.BigIntegerField(default=0)
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
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): class InvenTreeAttachment(models.Model):

View File

@ -16,6 +16,7 @@ from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ValidationError as DjangoValidationError from django.core.exceptions import ValidationError as DjangoValidationError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.db import models
from djmoney.contrib.django_rest_framework.fields import MoneyField from djmoney.contrib.django_rest_framework.fields import MoneyField
from djmoney.money import Money from djmoney.money import Money
@ -27,6 +28,8 @@ from rest_framework.fields import empty
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from rest_framework.serializers import DecimalField from rest_framework.serializers import DecimalField
from .models import extract_int
class InvenTreeMoneySerializer(MoneyField): class InvenTreeMoneySerializer(MoneyField):
""" """
@ -239,6 +242,17 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
return data 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): class InvenTreeAttachmentSerializerField(serializers.FileField):
""" """
Override the DRF native FileField serializer, Override the DRF native FileField serializer,

View File

@ -781,6 +781,7 @@ input[type="submit"] {
.btn-small { .btn-small {
padding: 3px; padding: 3px;
padding-left: 5px; padding-left: 5px;
padding-right: 5px;
} }
.btn-remove { .btn-remove {

View File

@ -12,11 +12,15 @@ import common.models
INVENTREE_SW_VERSION = "0.6.0 dev" INVENTREE_SW_VERSION = "0.6.0 dev"
# InvenTree API version # 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 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 v18 -> 2021-11-15
- Adds the ability to filter BomItem API by "uses" field - Adds the ability to filter BomItem API by "uses" field
- This returns a list of all BomItems which "use" the specified part - This returns a list of all BomItems which "use" the specified part

View File

@ -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),
),
]

View File

@ -16,7 +16,7 @@ from rest_framework import serializers
from rest_framework.serializers import ValidationError from rest_framework.serializers import ValidationError
from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentSerializer from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentSerializer
from InvenTree.serializers import UserSerializerBrief from InvenTree.serializers import UserSerializerBrief, ReferenceIndexingSerializerMixin
import InvenTree.helpers import InvenTree.helpers
from InvenTree.serializers import InvenTreeDecimalField from InvenTree.serializers import InvenTreeDecimalField
@ -32,7 +32,7 @@ from users.serializers import OwnerSerializer
from .models import Build, BuildItem, BuildOrderAttachment from .models import Build, BuildItem, BuildOrderAttachment
class BuildSerializer(InvenTreeModelSerializer): class BuildSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
""" """
Serializes a Build object Serializes a Build object
""" """

View File

@ -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),
),
]

View File

@ -24,6 +24,7 @@ from InvenTree.serializers import InvenTreeAttachmentSerializer
from InvenTree.serializers import InvenTreeModelSerializer from InvenTree.serializers import InvenTreeModelSerializer
from InvenTree.serializers import InvenTreeDecimalField from InvenTree.serializers import InvenTreeDecimalField
from InvenTree.serializers import InvenTreeMoneySerializer from InvenTree.serializers import InvenTreeMoneySerializer
from InvenTree.serializers import ReferenceIndexingSerializerMixin
from InvenTree.status_codes import StockStatus from InvenTree.status_codes import StockStatus
from part.serializers import PartBriefSerializer from part.serializers import PartBriefSerializer
@ -39,7 +40,7 @@ from .models import SalesOrderAllocation
from users.serializers import OwnerSerializer from users.serializers import OwnerSerializer
class POSerializer(InvenTreeModelSerializer): class POSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
""" Serializer for a PurchaseOrder object """ """ Serializer for a PurchaseOrder object """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -394,7 +395,7 @@ class POAttachmentSerializer(InvenTreeAttachmentSerializer):
] ]
class SalesOrderSerializer(InvenTreeModelSerializer): class SalesOrderSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
""" """
Serializers for the SalesOrder object Serializers for the SalesOrder object
""" """

View File

@ -105,6 +105,25 @@ class PurchaseOrderTest(OrderTest):
self.assertEqual(data['pk'], 1) self.assertEqual(data['pk'], 1)
self.assertEqual(data['description'], 'Ordering some screws') 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): def test_po_attachments(self):
url = reverse('api-po-attachment-list') url = reverse('api-po-attachment-list')

View File

@ -1075,6 +1075,7 @@ class PartList(generics.ListCreateAPIView):
'revision', 'revision',
'keywords', 'keywords',
'category__name', 'category__name',
'manufacturer_parts__MPN',
] ]

View File

@ -322,7 +322,14 @@
<tr> <tr>
<td><span class='fas fa-hashtag'></span></td> <td><span class='fas fa-hashtag'></span></td>
<td>{% trans "Latest Serial Number" %}</td> <td>{% trans "Latest Serial Number" %}</td>
<td>{{ part.getLatestSerialNumber }}{% include "clip.html"%}</td> <td>
{{ part.getLatestSerialNumber }}
<div class='btn-group float-right' role='group'>
<a class='btn btn-small btn-outline-secondary text-sm' href='#' id='serial-number-search' title='{% trans "Search for serial number" %}'>
<span class='fas fa-search'></span>
</a>
</div>
</td>
</tr> </tr>
{% endif %} {% endif %}
{% if part.default_location %} {% if part.default_location %}
@ -577,4 +584,8 @@
$('#collapse-part-details').collapse('show'); $('#collapse-part-details').collapse('show');
} }
$('#serial-number-search').click(function() {
findStockItemBySerialNumber({{ part.pk }});
});
{% endblock %} {% endblock %}

View File

@ -313,7 +313,7 @@ class StockFilter(rest_filters.FilterSet):
# Serial number filtering # Serial number filtering
serial_gte = rest_filters.NumberFilter(label='Serial number GTE', field_name='serial', lookup_expr='gte') 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_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') serialized = rest_filters.BooleanFilter(label='Has serial number', method='filter_serialized')
@ -703,6 +703,18 @@ class StockList(generics.ListCreateAPIView):
except (ValueError, StockItem.DoesNotExist): except (ValueError, StockItem.DoesNotExist):
pass 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? # Filter by 'allocated' parts?
allocated = params.get('allocated', None) allocated = params.get('allocated', None)

View File

@ -7,7 +7,6 @@ Stock database model definitions
from __future__ import unicode_literals from __future__ import unicode_literals
import os import os
import re
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError, FieldError from django.core.exceptions import ValidationError, FieldError
@ -39,6 +38,7 @@ import label.models
from InvenTree.status_codes import StockStatus, StockHistoryCode from InvenTree.status_codes import StockStatus, StockHistoryCode
from InvenTree.models import InvenTreeTree, InvenTreeAttachment from InvenTree.models import InvenTreeTree, InvenTreeAttachment
from InvenTree.fields import InvenTreeModelMoneyField, InvenTreeURLField from InvenTree.fields import InvenTreeModelMoneyField, InvenTreeURLField
from InvenTree.serializers import extract_int
from users.models import Owner from users.models import Owner
@ -236,17 +236,7 @@ class StockItem(MPTTModel):
serial_int = 0 serial_int = 0
if serial is not None: if serial is not None:
serial_int = extract_int(str(serial))
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
self.serial_int = serial_int self.serial_int = serial_int

View File

@ -32,7 +32,7 @@ from company.serializers import SupplierPartSerializer
import InvenTree.helpers import InvenTree.helpers
import InvenTree.serializers import InvenTree.serializers
from InvenTree.serializers import InvenTreeDecimalField from InvenTree.serializers import InvenTreeDecimalField, extract_int
from part.serializers import PartBriefSerializer from part.serializers import PartBriefSerializer
@ -73,6 +73,11 @@ class StockItemSerializerBrief(InvenTree.serializers.InvenTreeModelSerializer):
'uid', '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): class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
""" Serializer for a StockItem: """ Serializer for a StockItem:

View File

@ -148,17 +148,24 @@
<td><span class='fas fa-hashtag'></span></td> <td><span class='fas fa-hashtag'></span></td>
<td>{% trans "Serial Number" %}</td> <td>{% trans "Serial Number" %}</td>
<td> <td>
{% if previous %} {{ item.serial }}
<a class="btn btn-outline-secondary" aria-label="{% trans 'previous page' %}" href="{% url request.resolver_match.url_name previous.id %}"> <div class='btn-group float-right' role='group'>
<small>{{ previous.serial }}</small> {% if previous %}
</a> <a class="btn btn-small btn-outline-secondary" aria-label="{% trans 'previous page' %}" href="{% url request.resolver_match.url_name previous.id %}" title='{% trans "Navigate to previous serial number" %}'>
{% endif %} <span class='fas fa-angle-left'></span>
{{ item.serial }} <small>{{ previous.serial }}</small>
{% if next %} </a>
<a class="btn btn-outline-secondary text-sm" aria-label="{% trans 'next page' %}" href="{% url request.resolver_match.url_name next.id %}"> {% endif %}
<small>{{ next.serial }}</small> <a class='btn btn-small btn-outline-secondary text-sm' href='#' id='serial-number-search' title='{% trans "Search for serial number" %}'>
</a> <span class='fas fa-search'></span>
{% endif %} </a>
{% if next %}
<a class="btn btn-small btn-outline-secondary text-sm" aria-label="{% trans 'next page' %}" href="{% url request.resolver_match.url_name next.id %}" title='{% trans "Navigate to next serial number" %}'>
<small>{{ next.serial }}</small>
<span class='fas fa-angle-right'></span>
</a>
{% endif %}
</div>
</td> </td>
</tr> </tr>
{% else %} {% else %}
@ -592,4 +599,8 @@ $("#stock-return-from-customer").click(function() {
{% endif %} {% endif %}
$('#serial-number-search').click(function() {
findStockItemBySerialNumber({{ item.part.pk }});
});
{% endblock %} {% endblock %}

View File

@ -121,7 +121,6 @@
</div> </div>
{% include 'modals.html' %} {% include 'modals.html' %}
{% include 'about.html' %} {% include 'about.html' %}
{% include "notifications.html" %}
</div> </div>
<!-- Scripts --> <!-- Scripts -->

View File

@ -44,6 +44,7 @@
editStockItem, editStockItem,
editStockLocation, editStockLocation,
exportStock, exportStock,
findStockItemBySerialNumber,
loadInstalledInTable, loadInstalledInTable,
loadStockLocationTable, loadStockLocationTable,
loadStockTable, loadStockTable,
@ -394,6 +395,87 @@ function createNewStockItem(options={}) {
constructForm(url, 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 /* Stock API functions
* Requires api.js to be loaded first * Requires api.js to be loaded first

7
package.json Normal file
View File

@ -0,0 +1,7 @@
{
"dependencies": {
"eslint": "^8.3.0",
"eslint-config-google": "^0.14.0",
"markuplint": "^1.11.4"
}
}