mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'master' of https://github.com/inventree/InvenTree into workflow-remaster
This commit is contained in:
commit
f71ac93668
1
.gitignore
vendored
1
.gitignore
vendored
@ -78,5 +78,4 @@ locale_stats.json
|
||||
|
||||
# node.js
|
||||
package-lock.json
|
||||
package.json
|
||||
node_modules/
|
@ -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):
|
||||
|
@ -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,
|
||||
|
@ -781,6 +781,7 @@ input[type="submit"] {
|
||||
.btn-small {
|
||||
padding: 3px;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.btn-remove {
|
||||
|
@ -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
|
||||
|
18
InvenTree/build/migrations/0034_alter_build_reference_int.py
Normal file
18
InvenTree/build/migrations/0034_alter_build_reference_int.py
Normal 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),
|
||||
),
|
||||
]
|
@ -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
|
||||
"""
|
||||
|
23
InvenTree/order/migrations/0054_auto_20211201_2139.py
Normal file
23
InvenTree/order/migrations/0054_auto_20211201_2139.py
Normal 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),
|
||||
),
|
||||
]
|
@ -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
|
||||
"""
|
||||
|
@ -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')
|
||||
|
@ -1075,6 +1075,7 @@ class PartList(generics.ListCreateAPIView):
|
||||
'revision',
|
||||
'keywords',
|
||||
'category__name',
|
||||
'manufacturer_parts__MPN',
|
||||
]
|
||||
|
||||
|
||||
|
@ -322,7 +322,14 @@
|
||||
<tr>
|
||||
<td><span class='fas fa-hashtag'></span></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>
|
||||
{% endif %}
|
||||
{% if part.default_location %}
|
||||
@ -577,4 +584,8 @@
|
||||
$('#collapse-part-details').collapse('show');
|
||||
}
|
||||
|
||||
$('#serial-number-search').click(function() {
|
||||
findStockItemBySerialNumber({{ part.pk }});
|
||||
});
|
||||
|
||||
{% endblock %}
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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:
|
||||
|
@ -148,17 +148,24 @@
|
||||
<td><span class='fas fa-hashtag'></span></td>
|
||||
<td>{% trans "Serial Number" %}</td>
|
||||
<td>
|
||||
{% if previous %}
|
||||
<a class="btn btn-outline-secondary" aria-label="{% trans 'previous page' %}" href="{% url request.resolver_match.url_name previous.id %}">
|
||||
<small>{{ previous.serial }}</small> ‹
|
||||
</a>
|
||||
{% endif %}
|
||||
{{ item.serial }}
|
||||
{% if next %}
|
||||
<a class="btn btn-outline-secondary text-sm" aria-label="{% trans 'next page' %}" href="{% url request.resolver_match.url_name next.id %}">
|
||||
› <small>{{ next.serial }}</small>
|
||||
</a>
|
||||
{% endif %}
|
||||
{{ item.serial }}
|
||||
<div class='btn-group float-right' role='group'>
|
||||
{% if previous %}
|
||||
<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" %}'>
|
||||
<span class='fas fa-angle-left'></span>
|
||||
<small>{{ previous.serial }}</small>
|
||||
</a>
|
||||
{% endif %}
|
||||
<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>
|
||||
{% 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>
|
||||
</tr>
|
||||
{% else %}
|
||||
@ -592,4 +599,8 @@ $("#stock-return-from-customer").click(function() {
|
||||
|
||||
{% endif %}
|
||||
|
||||
$('#serial-number-search').click(function() {
|
||||
findStockItemBySerialNumber({{ item.part.pk }});
|
||||
});
|
||||
|
||||
{% endblock %}
|
||||
|
@ -121,7 +121,6 @@
|
||||
</div>
|
||||
{% include 'modals.html' %}
|
||||
{% include 'about.html' %}
|
||||
{% include "notifications.html" %}
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
|
@ -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
|
||||
|
7
package.json
Normal file
7
package.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"eslint": "^8.3.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"markuplint": "^1.11.4"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user