Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2020-02-11 20:36:13 +11:00
commit bb4c25ba68
20 changed files with 793 additions and 168 deletions

View File

@ -308,7 +308,10 @@ STATICFILES_DIRS = [
MEDIA_URL = '/media/'
# The filesystem location for served static files
MEDIA_ROOT = CONFIG.get('media_root', os.path.join(BASE_DIR, 'media'))
MEDIA_ROOT = os.path.abspath(CONFIG.get('media_root', os.path.join(BASE_DIR, 'media')))
if DEBUG:
print("MEDIA_ROOT:", MEDIA_ROOT)
# crispy forms use the bootstrap templates
CRISPY_TEMPLATE_PACK = 'bootstrap'

View File

@ -183,6 +183,24 @@
-webkit-opacity: 10%;
}
/* grid display for part images */
.table-img-grid tr {
display: inline;
}
.table-img-grid td {
padding: 10px;
margin: 10px;
}
.table-img-grid .grid-image {
height: 128px;
width: 128px;
object-fit: contain;
background: #eee;
}
.btn-glyph {
padding-left: 6px;
@ -211,6 +229,20 @@
object-fit: contain;
}
.part-thumb-container:hover .part-thumb-overlay {
opacity: 1;
}
.part-thumb-overlay {
position: absolute;
top: 0;
left: 0;
opacity: 0;
transition: .25s ease;
padding: 15px;
margin: 5px;
}
.checkbox {
margin-left: 20px;
}

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.9 on 2020-02-10 10:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('build', '0008_auto_20200201_1247'),
]
operations = [
migrations.AlterField(
model_name='build',
name='creation_date',
field=models.DateField(auto_now_add=True),
),
]

View File

@ -82,7 +82,7 @@ class Build(models.Model):
batch = models.CharField(max_length=100, blank=True, null=True,
help_text=_('Batch code for this build output'))
creation_date = models.DateField(auto_now=True, editable=False)
creation_date = models.DateField(auto_now_add=True, editable=False)
completion_date = models.DateField(null=True, blank=True)

View File

@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-02-03 10:28+0000\n"
"POT-Creation-Date: 2020-02-10 11:09+0000\n"
"PO-Revision-Date: 2020-02-02 08:07+0100\n"
"Last-Translator: Christian Schlüter <chschlue@gmail.com>\n"
"Language-Team: C <kde-i18n-doc@kde.org>\n"
@ -706,7 +706,7 @@ msgstr "Link auf externe Seite"
msgid "Order notes"
msgstr "Bestell-Notizen"
#: order/models.py:159 order/models.py:210 part/views.py:1067
#: order/models.py:159 order/models.py:210 part/views.py:1080
#: stock/models.py:440
msgid "Quantity must be greater than zero"
msgstr "Anzahl muss größer Null sein"
@ -1277,56 +1277,208 @@ msgstr "Tracking"
msgid "Attachments"
msgstr "Anhänge"
#: part/views.py:77
#, fuzzy
#| msgid "Add Attachment"
msgid "Added attachment"
msgstr "Anhang hinzufügen"
#: part/views.py:119
#, fuzzy
#| msgid "Part Attachments"
msgid "Part attachment updated"
msgstr "Anhänge"
#: part/views.py:196
#, python-brace-format
msgid "Set category for {n} parts"
msgstr "Kategorie für {n} Teile setzen"
#: part/views.py:808
#: part/views.py:306
#, fuzzy
#| msgid "Supplier part"
msgid "Copied part"
msgstr "Zulieferer-Teil"
#: part/views.py:414
#, fuzzy
#| msgid "Create new Stock Item"
msgid "Create new part"
msgstr "Neues Lagerobjekt hinzufügen"
#: part/views.py:419
#, fuzzy
#| msgid "Created new stock item"
msgid "Created new part"
msgstr "Neues Lagerobjekt erstellt"
#: part/views.py:609
msgid "Upload Part Image"
msgstr ""
#: part/views.py:614
msgid "Updated part image"
msgstr ""
#: part/views.py:623
#, fuzzy
#| msgid "Select part"
msgid "Select Part Image"
msgstr "Teil auswählen"
#: part/views.py:627
#, fuzzy
#| msgid "Select part"
msgid "Selected part image"
msgstr "Teil auswählen"
#: part/views.py:637
#, fuzzy
#| msgid "Edit notes"
msgid "Edit Part Properties"
msgstr "Bermerkungen bearbeiten"
#: part/views.py:659
msgid "Validate BOM"
msgstr ""
#: part/views.py:821
msgid "No BOM file provided"
msgstr "Keine Stückliste angegeben"
#: part/views.py:1069
#: part/views.py:1082
msgid "Enter a valid quantity"
msgstr "Bitte eine gültige Anzahl eingeben"
#: part/views.py:1093 part/views.py:1096
#: part/views.py:1106 part/views.py:1109
msgid "Select valid part"
msgstr "Bitte ein gültiges Teil auswählen"
#: part/views.py:1102
#: part/views.py:1115
msgid "Duplicate part selected"
msgstr "Teil doppelt ausgewählt"
#: part/views.py:1130
#: part/views.py:1143
msgid "Select a part"
msgstr "Teil auswählen"
#: part/views.py:1134
#: part/views.py:1147
msgid "Specify quantity"
msgstr "Anzahl angeben"
#: stock/forms.py:92
#: part/views.py:1324
#, fuzzy
#| msgid "Confirm part creation"
msgid "Confirm Part Deletion"
msgstr "Erstellen des Teils bestätigen"
#: part/views.py:1331
msgid "Part was deleted"
msgstr ""
#: part/views.py:1340
#, fuzzy
#| msgid "Part packaging"
msgid "Part Pricing"
msgstr "Teile-Packaging"
#: part/views.py:1462
#, fuzzy
#| msgid "Parameter Template"
msgid "Create Part Parameter Template"
msgstr "Parameter Vorlage"
#: part/views.py:1470
#, fuzzy
#| msgid "Parameter Template"
msgid "Edit Part Parameter Template"
msgstr "Parameter Vorlage"
#: part/views.py:1477
#, fuzzy
#| msgid "Parameter Template"
msgid "Delete Part Parameter Template"
msgstr "Parameter Vorlage"
#: part/views.py:1485
msgid "Create Part Parameter"
msgstr ""
#: part/views.py:1535
#, fuzzy
#| msgid "Edit attachment"
msgid "Edit Part Parameter"
msgstr "Anhang bearbeiten"
#: part/views.py:1549
#, fuzzy
#| msgid "Delete attachment"
msgid "Delete Part Parameter"
msgstr "Anhang löschen"
#: part/views.py:1565
#, fuzzy
#| msgid "Part category"
msgid "Edit Part Category"
msgstr "Teile-Kategorie"
#: part/views.py:1600
#, fuzzy
#| msgid "Select part category"
msgid "Delete Part Category"
msgstr "Teilekategorie wählen"
#: part/views.py:1606
#, fuzzy
#| msgid "Part category"
msgid "Part category was deleted"
msgstr "Teile-Kategorie"
#: part/views.py:1614
#, fuzzy
#| msgid "Select part category"
msgid "Create new part category"
msgstr "Teilekategorie wählen"
#: part/views.py:1665
#, fuzzy
#| msgid "Created new stock item"
msgid "Create BOM item"
msgstr "Neues Lagerobjekt erstellt"
#: part/views.py:1731
#, fuzzy
#| msgid "Edit Stock Item"
msgid "Edit BOM item"
msgstr "Lagerobjekt bearbeiten"
#: part/views.py:1779
#, fuzzy
#| msgid "Confirm build completion"
msgid "Confim BOM item deletion"
msgstr "Bau-Fertigstellung bestätigen"
#: stock/forms.py:91
msgid "File Format"
msgstr "Dateiformat"
#: stock/forms.py:92
#: stock/forms.py:91
msgid "Select output file format"
msgstr "Ausgabe-Dateiformat auswählen"
#: stock/forms.py:94
#: stock/forms.py:93
msgid "Include stock items in sub locations"
msgstr "Lagerobjekte in untergeordneten Lagerorten einschließen"
#: stock/forms.py:127
#: stock/forms.py:126
msgid "Destination stock location"
msgstr "Ziel-Lagerbestand"
#: stock/forms.py:133
#: stock/forms.py:132
msgid "Confirm movement of stock items"
msgstr "Bewegung der Lagerobjekte bestätigen"
#: stock/forms.py:135
#: stock/forms.py:134
msgid "Set the destination as the default location for selected parts"
msgstr "Setze das Ziel als Standard-Ziel für ausgewählte Teile"
@ -1652,27 +1804,33 @@ msgstr "Ungültige Menge"
msgid "Invalid part selection"
msgstr "Ungültige Teileauswahl"
#: stock/views.py:925
#: stock/views.py:910
#, fuzzy, python-brace-format
#| msgid "Created new stock item"
msgid "Created {n} new stock items"
msgstr "Neues Lagerobjekt erstellt"
#: stock/views.py:927 stock/views.py:940
msgid "Created new stock item"
msgstr "Neues Lagerobjekt erstellt"
#: stock/views.py:942
#: stock/views.py:957
msgid "Delete Stock Location"
msgstr "Standort löschen"
#: stock/views.py:955
#: stock/views.py:970
msgid "Delete Stock Item"
msgstr "Lagerobjekt löschen"
#: stock/views.py:966
#: stock/views.py:981
msgid "Delete Stock Tracking Entry"
msgstr "Lagerbestands-Tracking-Eintrag löschen"
#: stock/views.py:983
#: stock/views.py:998
msgid "Edit Stock Tracking Entry"
msgstr "Lagerbestands-Tracking-Eintrag bearbeiten"
#: stock/views.py:992
#: stock/views.py:1007
msgid "Add Stock Tracking Entry"
msgstr "Lagerbestands-Tracking-Eintrag hinzufügen"

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-02-03 10:28+0000\n"
"POT-Creation-Date: 2020-02-10 11:09+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -675,7 +675,7 @@ msgstr ""
msgid "Order notes"
msgstr ""
#: order/models.py:159 order/models.py:210 part/views.py:1067
#: order/models.py:159 order/models.py:210 part/views.py:1080
#: stock/models.py:440
msgid "Quantity must be greater than zero"
msgstr ""
@ -1242,56 +1242,164 @@ msgstr ""
msgid "Attachments"
msgstr ""
#: part/views.py:77
msgid "Added attachment"
msgstr ""
#: part/views.py:119
msgid "Part attachment updated"
msgstr ""
#: part/views.py:196
#, python-brace-format
msgid "Set category for {n} parts"
msgstr ""
#: part/views.py:808
#: part/views.py:306
msgid "Copied part"
msgstr ""
#: part/views.py:414
msgid "Create new part"
msgstr ""
#: part/views.py:419
msgid "Created new part"
msgstr ""
#: part/views.py:609
msgid "Upload Part Image"
msgstr ""
#: part/views.py:614
msgid "Updated part image"
msgstr ""
#: part/views.py:623
msgid "Select Part Image"
msgstr ""
#: part/views.py:627
msgid "Selected part image"
msgstr ""
#: part/views.py:637
msgid "Edit Part Properties"
msgstr ""
#: part/views.py:659
msgid "Validate BOM"
msgstr ""
#: part/views.py:821
msgid "No BOM file provided"
msgstr ""
#: part/views.py:1069
#: part/views.py:1082
msgid "Enter a valid quantity"
msgstr ""
#: part/views.py:1093 part/views.py:1096
#: part/views.py:1106 part/views.py:1109
msgid "Select valid part"
msgstr ""
#: part/views.py:1102
#: part/views.py:1115
msgid "Duplicate part selected"
msgstr ""
#: part/views.py:1130
#: part/views.py:1143
msgid "Select a part"
msgstr ""
#: part/views.py:1134
#: part/views.py:1147
msgid "Specify quantity"
msgstr ""
#: stock/forms.py:92
#: part/views.py:1324
msgid "Confirm Part Deletion"
msgstr ""
#: part/views.py:1331
msgid "Part was deleted"
msgstr ""
#: part/views.py:1340
msgid "Part Pricing"
msgstr ""
#: part/views.py:1462
msgid "Create Part Parameter Template"
msgstr ""
#: part/views.py:1470
msgid "Edit Part Parameter Template"
msgstr ""
#: part/views.py:1477
msgid "Delete Part Parameter Template"
msgstr ""
#: part/views.py:1485
msgid "Create Part Parameter"
msgstr ""
#: part/views.py:1535
msgid "Edit Part Parameter"
msgstr ""
#: part/views.py:1549
msgid "Delete Part Parameter"
msgstr ""
#: part/views.py:1565
msgid "Edit Part Category"
msgstr ""
#: part/views.py:1600
msgid "Delete Part Category"
msgstr ""
#: part/views.py:1606
msgid "Part category was deleted"
msgstr ""
#: part/views.py:1614
msgid "Create new part category"
msgstr ""
#: part/views.py:1665
msgid "Create BOM item"
msgstr ""
#: part/views.py:1731
msgid "Edit BOM item"
msgstr ""
#: part/views.py:1779
msgid "Confim BOM item deletion"
msgstr ""
#: stock/forms.py:91
msgid "File Format"
msgstr ""
#: stock/forms.py:92
#: stock/forms.py:91
msgid "Select output file format"
msgstr ""
#: stock/forms.py:94
#: stock/forms.py:93
msgid "Include stock items in sub locations"
msgstr ""
#: stock/forms.py:127
#: stock/forms.py:126
msgid "Destination stock location"
msgstr ""
#: stock/forms.py:133
#: stock/forms.py:132
msgid "Confirm movement of stock items"
msgstr ""
#: stock/forms.py:135
#: stock/forms.py:134
msgid "Set the destination as the default location for selected parts"
msgstr ""
@ -1610,27 +1718,32 @@ msgstr ""
msgid "Invalid part selection"
msgstr ""
#: stock/views.py:925
#: stock/views.py:910
#, python-brace-format
msgid "Created {n} new stock items"
msgstr ""
#: stock/views.py:927 stock/views.py:940
msgid "Created new stock item"
msgstr ""
#: stock/views.py:942
#: stock/views.py:957
msgid "Delete Stock Location"
msgstr ""
#: stock/views.py:955
#: stock/views.py:970
msgid "Delete Stock Item"
msgstr ""
#: stock/views.py:966
#: stock/views.py:981
msgid "Delete Stock Tracking Entry"
msgstr ""
#: stock/views.py:983
#: stock/views.py:998
msgid "Edit Stock Tracking Entry"
msgstr ""
#: stock/views.py:992
#: stock/views.py:1007
msgid "Add Stock Tracking Entry"
msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-02-03 10:28+0000\n"
"POT-Creation-Date: 2020-02-10 11:09+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -675,7 +675,7 @@ msgstr ""
msgid "Order notes"
msgstr ""
#: order/models.py:159 order/models.py:210 part/views.py:1067
#: order/models.py:159 order/models.py:210 part/views.py:1080
#: stock/models.py:440
msgid "Quantity must be greater than zero"
msgstr ""
@ -1242,56 +1242,164 @@ msgstr ""
msgid "Attachments"
msgstr ""
#: part/views.py:77
msgid "Added attachment"
msgstr ""
#: part/views.py:119
msgid "Part attachment updated"
msgstr ""
#: part/views.py:196
#, python-brace-format
msgid "Set category for {n} parts"
msgstr ""
#: part/views.py:808
#: part/views.py:306
msgid "Copied part"
msgstr ""
#: part/views.py:414
msgid "Create new part"
msgstr ""
#: part/views.py:419
msgid "Created new part"
msgstr ""
#: part/views.py:609
msgid "Upload Part Image"
msgstr ""
#: part/views.py:614
msgid "Updated part image"
msgstr ""
#: part/views.py:623
msgid "Select Part Image"
msgstr ""
#: part/views.py:627
msgid "Selected part image"
msgstr ""
#: part/views.py:637
msgid "Edit Part Properties"
msgstr ""
#: part/views.py:659
msgid "Validate BOM"
msgstr ""
#: part/views.py:821
msgid "No BOM file provided"
msgstr ""
#: part/views.py:1069
#: part/views.py:1082
msgid "Enter a valid quantity"
msgstr ""
#: part/views.py:1093 part/views.py:1096
#: part/views.py:1106 part/views.py:1109
msgid "Select valid part"
msgstr ""
#: part/views.py:1102
#: part/views.py:1115
msgid "Duplicate part selected"
msgstr ""
#: part/views.py:1130
#: part/views.py:1143
msgid "Select a part"
msgstr ""
#: part/views.py:1134
#: part/views.py:1147
msgid "Specify quantity"
msgstr ""
#: stock/forms.py:92
#: part/views.py:1324
msgid "Confirm Part Deletion"
msgstr ""
#: part/views.py:1331
msgid "Part was deleted"
msgstr ""
#: part/views.py:1340
msgid "Part Pricing"
msgstr ""
#: part/views.py:1462
msgid "Create Part Parameter Template"
msgstr ""
#: part/views.py:1470
msgid "Edit Part Parameter Template"
msgstr ""
#: part/views.py:1477
msgid "Delete Part Parameter Template"
msgstr ""
#: part/views.py:1485
msgid "Create Part Parameter"
msgstr ""
#: part/views.py:1535
msgid "Edit Part Parameter"
msgstr ""
#: part/views.py:1549
msgid "Delete Part Parameter"
msgstr ""
#: part/views.py:1565
msgid "Edit Part Category"
msgstr ""
#: part/views.py:1600
msgid "Delete Part Category"
msgstr ""
#: part/views.py:1606
msgid "Part category was deleted"
msgstr ""
#: part/views.py:1614
msgid "Create new part category"
msgstr ""
#: part/views.py:1665
msgid "Create BOM item"
msgstr ""
#: part/views.py:1731
msgid "Edit BOM item"
msgstr ""
#: part/views.py:1779
msgid "Confim BOM item deletion"
msgstr ""
#: stock/forms.py:91
msgid "File Format"
msgstr ""
#: stock/forms.py:92
#: stock/forms.py:91
msgid "Select output file format"
msgstr ""
#: stock/forms.py:94
#: stock/forms.py:93
msgid "Include stock items in sub locations"
msgstr ""
#: stock/forms.py:127
#: stock/forms.py:126
msgid "Destination stock location"
msgstr ""
#: stock/forms.py:133
#: stock/forms.py:132
msgid "Confirm movement of stock items"
msgstr ""
#: stock/forms.py:135
#: stock/forms.py:134
msgid "Set the destination as the default location for selected parts"
msgstr ""
@ -1610,27 +1718,32 @@ msgstr ""
msgid "Invalid part selection"
msgstr ""
#: stock/views.py:925
#: stock/views.py:910
#, python-brace-format
msgid "Created {n} new stock items"
msgstr ""
#: stock/views.py:927 stock/views.py:940
msgid "Created new stock item"
msgstr ""
#: stock/views.py:942
#: stock/views.py:957
msgid "Delete Stock Location"
msgstr ""
#: stock/views.py:955
#: stock/views.py:970
msgid "Delete Stock Item"
msgstr ""
#: stock/views.py:966
#: stock/views.py:981
msgid "Delete Stock Tracking Entry"
msgstr ""
#: stock/views.py:983
#: stock/views.py:998
msgid "Edit Stock Tracking Entry"
msgstr ""
#: stock/views.py:992
#: stock/views.py:1007
msgid "Add Stock Tracking Entry"
msgstr ""

View File

@ -8,7 +8,7 @@ from __future__ import unicode_literals
from django_filters.rest_framework import DjangoFilterBackend
from django.conf import settings
from django.db.models import Sum
from django.db.models import Sum, Count
from rest_framework import status
from rest_framework.response import Response
@ -23,10 +23,7 @@ import os
from .models import Part, PartCategory, BomItem, PartStar
from .models import PartParameter, PartParameterTemplate
from .serializers import PartSerializer, BomItemSerializer
from .serializers import CategorySerializer
from .serializers import PartStarSerializer
from .serializers import PartParameterSerializer, PartParameterTemplateSerializer
from . import serializers as part_serializers
from InvenTree.views import TreeSerializer
from InvenTree.helpers import str2bool
@ -53,7 +50,7 @@ class CategoryList(generics.ListCreateAPIView):
"""
queryset = PartCategory.objects.all()
serializer_class = CategorySerializer
serializer_class = part_serializers.CategorySerializer
permission_classes = [
permissions.IsAuthenticated,
@ -83,14 +80,37 @@ class CategoryList(generics.ListCreateAPIView):
class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
""" API endpoint for detail view of a single PartCategory object """
serializer_class = CategorySerializer
serializer_class = part_serializers.CategorySerializer
queryset = PartCategory.objects.all()
class PartThumbs(generics.ListAPIView):
""" API endpoint for retrieving information on available Part thumbnails """
serializer_class = part_serializers.PartThumbSerializer
def list(self, reguest, *args, **kwargs):
"""
Serialize the available Part images.
- Images may be used for multiple parts!
"""
# Get all Parts which have an associated image
queryset = Part.objects.all().exclude(image='')
# Return the most popular parts first
data = queryset.values(
'image',
).annotate(count=Count('image')).order_by('-count')
return Response(data)
class PartDetail(generics.RetrieveUpdateAPIView):
""" API endpoint for detail view of a single Part object """
queryset = Part.objects.all()
serializer_class = PartSerializer
serializer_class = part_serializers.PartSerializer
permission_classes = [
permissions.IsAuthenticated,
@ -104,12 +124,12 @@ class PartList(generics.ListCreateAPIView):
- POST: Create a new Part object
"""
serializer_class = PartSerializer
serializer_class = part_serializers.PartSerializer
def list(self, request, *args, **kwargs):
"""
Instead of using the DRF serialiser to LIST,
we serialize the objects manuually.
we serialize the objects manually.
This turns out to be significantly faster.
"""
@ -218,7 +238,7 @@ class PartStarDetail(generics.RetrieveDestroyAPIView):
""" API endpoint for viewing or removing a PartStar object """
queryset = PartStar.objects.all()
serializer_class = PartStarSerializer
serializer_class = part_serializers.PartStarSerializer
class PartStarList(generics.ListCreateAPIView):
@ -229,7 +249,7 @@ class PartStarList(generics.ListCreateAPIView):
"""
queryset = PartStar.objects.all()
serializer_class = PartStarSerializer
serializer_class = part_serializers.PartStarSerializer
def create(self, request, *args, **kwargs):
@ -271,7 +291,7 @@ class PartParameterTemplateList(generics.ListCreateAPIView):
"""
queryset = PartParameterTemplate.objects.all()
serializer_class = PartParameterTemplateSerializer
serializer_class = part_serializers.PartParameterTemplateSerializer
permission_classes = [
permissions.IsAuthenticated,
@ -294,7 +314,7 @@ class PartParameterList(generics.ListCreateAPIView):
"""
queryset = PartParameter.objects.all()
serializer_class = PartParameterSerializer
serializer_class = part_serializers.PartParameterSerializer
permission_classes = [
permissions.IsAuthenticated,
@ -317,7 +337,7 @@ class BomList(generics.ListCreateAPIView):
- POST: Create a new BomItem object
"""
serializer_class = BomItemSerializer
serializer_class = part_serializers.BomItemSerializer
def get_serializer(self, *args, **kwargs):
@ -360,7 +380,7 @@ class BomDetail(generics.RetrieveUpdateDestroyAPIView):
""" API endpoint for detail view of a single BomItem object """
queryset = BomItem.objects.all()
serializer_class = BomItemSerializer
serializer_class = part_serializers.BomItemSerializer
permission_classes = [
permissions.IsAuthenticated,
@ -424,6 +444,8 @@ part_api_urls = [
url(r'^star/', include(part_star_api_urls)),
url(r'^parameter/', include(part_param_api_urls)),
url(r'^thumbs/', PartThumbs.as_view(), name='api-part-thumbs'),
url(r'^(?P<pk>\d+)/?', PartDetail.as_view(), name='api-part-detail'),
url(r'^.*$', PartList.as_view(), name='api-part-list'),

View File

@ -12,7 +12,6 @@ from django.core.exceptions import ValidationError
from django.urls import reverse
from django.conf import settings
from django.core.files.base import ContentFile
from django.db import models, transaction
from django.db.models import Sum
from django.db.models import prefetch_related_objects
@ -24,6 +23,8 @@ from django.dispatch import receiver
from markdownx.models import MarkdownxField
from django_cleanup import cleanup
from mptt.models import TreeForeignKey
from datetime import datetime
@ -136,18 +137,9 @@ def rename_part_image(instance, filename):
"""
base = 'part_images'
fname = os.path.basename(filename)
if filename.count('.') > 0:
ext = filename.split('.')[-1]
else:
ext = ''
fn = 'part_{pk}_img'.format(pk=instance.pk)
if ext:
fn += '.' + ext
return os.path.join(base, fn)
return os.path.join(base, fname)
def match_part_names(match, threshold=80, reverse=True, compare_length=False):
@ -201,6 +193,7 @@ def match_part_names(match, threshold=80, reverse=True, compare_length=False):
return matches
@cleanup.ignore
class Part(models.Model):
""" The Part object represents an abstract part, the 'concept' of an actual entity.
@ -237,6 +230,26 @@ class Part(models.Model):
verbose_name = "Part"
verbose_name_plural = "Parts"
def save(self, *args, **kwargs):
"""
Overrides the save() function for the Part model.
If the part image has been updated,
then check if the "old" (previous) image is still used by another part.
If not, it is considered "orphaned" and will be deleted.
"""
if self.pk:
previous = Part.objects.get(pk=self.pk)
if previous.image and not self.image == previous.image:
# Are there any (other) parts which reference the image?
n_refs = Part.objects.filter(image=previous.image).exclude(pk=self.pk).count()
if n_refs == 0:
previous.image.delete(save=False)
super().save(*args, **kwargs)
def __str__(self):
return "{n} - {d}".format(n=self.full_name, d=self.description)
@ -832,10 +845,8 @@ class Part(models.Model):
# Copy the part image
if kwargs.get('image', True):
if other.image:
image_file = ContentFile(other.image.read())
image_file.name = rename_part_image(self, other.image.url)
self.image = image_file
# Reference the other image from this Part
self.image = other.image
# Copy the BOM data
if kwargs.get('bom', False):

View File

@ -30,6 +30,16 @@ class CategorySerializer(InvenTreeModelSerializer):
]
class PartThumbSerializer(serializers.Serializer):
"""
Serializer for the 'image' field of the Part model.
Used to serve and display existing Part images.
"""
image = serializers.URLField(read_only=True)
count = serializers.IntegerField(read_only=True)
class PartBriefSerializer(InvenTreeModelSerializer):
""" Serializer for Part (brief detail) """

View File

@ -25,17 +25,7 @@
<div class="row">
<div class="col-sm-6">
<div class="media">
<div class="media-left">
<div class='dropzone' id='part-thumb'>
<img class="part-thumb"
{% if part.image %}
src="{{ part.image.url }}"
{% else %}
src="{% static 'img/blank_image.png' %}"
{% endif %}/>
</div>
</div>
{% include "part/part_thumb.html" %}
<div class="media-body">
<h4>
{{ part.full_name }}
@ -163,7 +153,7 @@
enableDragAndDrop(
'#part-thumb',
"{% url 'part-image' part.id %}",
"{% url 'part-image-upload' part.id %}",
{
label: 'image',
success: function(data, status, xhr) {
@ -208,13 +198,54 @@
});
});
$("#part-thumb").click(function() {
launchModalForm(
"{% url 'part-image' part.id %}",
$("#part-image-upload").click(function() {
launchModalForm("{% url 'part-image-upload' part.id %}",
{
reload: true
}
);
});
function onSelectImage(response) {
// Callback when the image-selection modal form is displayed
// Populate the form with image data (requested via AJAX)
$("#modal-form").find("#image-select-table").bootstrapTable({
pagination: true,
pageSize: 25,
url: "{% url 'api-part-thumbs' %}",
showHeader: false,
clickToSelect: true,
singleSelect: true,
columns: [
{
checkbox: true,
},
{
field: 'image',
title: 'Image',
formatter: function(value, row, index, field) {
return "<img src='/media/" + value + "' class='grid-image'/>"
}
}
],
onCheck: function(row, element) {
// Update the selected image in the form
var ipt = $("#modal-form").find("#image-input");
ipt.val(row.image);
}
});
}
$("#part-image-select").click(function() {
launchModalForm("{% url 'part-image-select' part.id %}",
{
reload: true
}
);
reload: true,
after_render: onSelectImage
});
});
$("#part-edit").click(function() {

View File

@ -0,0 +1,20 @@
{% load static %}
{% load i18n %}
<div class="media">
<div class="media-left part-thumb-container">
<div class='dropzone' id='part-thumb'>
<img class="part-thumb"
{% if part.image %}
src="{{ part.image.url }}"
{% else %}
src="{% static 'img/blank_image.png' %}"
{% endif %}/>
</div>
<div class='btn-row part-thumb-overlay'>
<div class='btn-group'>
<button type='button' class='btn btn-default btn-glyph' title="{% trans 'Select from existing images' %}" id='part-image-select'><span class='glyphicon glyphicon-th'></span></button>
<button type='button' class='btn btn-default btn-glyph' title="{% trans 'Upload new image' %}" id='part-image-upload'><span class='glyphicon glyphicon-upload'></span></button>
</div>
</div>
</div>

View File

@ -0,0 +1,21 @@
{% extends "modal_form.html" %}
{% block pre_form_content %}
{{ block.super }}
{% endblock %}
{% block form %}
<form method='post' action='' class='js-modal-form' enctype='multipart/form-data'>
{% csrf_token %}
{% load crispy_forms_tags %}
<input id='image-input' name='image' type='hidden' value="{{ part.image }}">
<table id='image-select-table' class='table table-striped table-condensed table-img-grid'>
</table>
</form>
{% endblock %}

View File

@ -23,7 +23,7 @@ class TemplateTagTest(TestCase):
def test_hash(self):
hash = inventree_extras.inventree_commit_hash()
self.assertEqual(len(hash), 7)
self.assertGreater(len(hash), 5)
def test_date(self):
d = inventree_extras.inventree_commit_date()
@ -68,11 +68,8 @@ class PartTest(TestCase):
def test_rename_img(self):
img = rename_part_image(self.R1, 'hello.png')
self.assertEqual(img, os.path.join('part_images', 'part_3_img.png'))
img = rename_part_image(self.R2, 'test')
self.assertEqual(img, os.path.join('part_images', 'part_4_img'))
self.assertEqual(img, os.path.join('part_images', 'hello.png'))
def test_stock(self):
# No stock of any resistors
res = Part.objects.filter(description__contains='resistor')

View File

@ -57,7 +57,8 @@ part_detail_urls = [
url(r'^qr_code/?', views.PartQRCode.as_view(), name='part-qr'),
# Normal thumbnail with form
url(r'^thumbnail/?', views.PartImage.as_view(), name='part-image'),
url(r'^thumbnail/?', views.PartImageUpload.as_view(), name='part-image-upload'),
url(r'^thumb-select/?', views.PartImageSelect.as_view(), name='part-image-select'),
# Any other URLs go to the part detail page
url(r'^.*$', views.PartDetail.as_view(), name='part-detail'),

View File

@ -14,6 +14,9 @@ from django.urls import reverse, reverse_lazy
from django.views.generic import DetailView, ListView, FormView, UpdateView
from django.forms.models import model_to_dict
from django.forms import HiddenInput, CheckboxInput
from django.conf import settings
import os
from fuzzywuzzy import fuzz
from decimal import Decimal
@ -74,7 +77,7 @@ class PartAttachmentCreate(AjaxCreateView):
def get_data(self):
return {
'success': 'Added attachment'
'success': _('Added attachment')
}
def get_initial(self):
@ -116,7 +119,7 @@ class PartAttachmentEdit(AjaxUpdateView):
def get_data(self):
return {
'success': 'Part attachment updated'
'success': _('Part attachment updated')
}
def get_form(self):
@ -303,7 +306,7 @@ class PartDuplicate(AjaxCreateView):
def get_data(self):
return {
'success': 'Copied part'
'success': _('Copied part')
}
def get_part_to_copy(self):
@ -411,12 +414,12 @@ class PartCreate(AjaxCreateView):
model = Part
form_class = part_forms.EditPartForm
ajax_form_title = 'Create new part'
ajax_form_title = _('Create new part')
ajax_template_name = 'part/create_part.html'
def get_data(self):
return {
'success': "Created new part",
'success': _("Created new part"),
}
def get_category_id(self):
@ -601,27 +604,66 @@ class PartQRCode(QRCodeView):
return None
class PartImage(AjaxUpdateView):
""" View for uploading Part image """
class PartImageUpload(AjaxUpdateView):
""" View for uploading a new Part image """
model = Part
ajax_template_name = 'modal_form.html'
ajax_form_title = 'Upload Part Image'
ajax_form_title = _('Upload Part Image')
form_class = part_forms.PartImageForm
def get_data(self):
return {
'success': 'Updated part image',
'success': _('Updated part image'),
}
class PartImageSelect(AjaxUpdateView):
""" View for selecting Part image from existing images. """
model = Part
ajax_template_name = 'part/select_image.html'
ajax_form_title = _('Select Part Image')
fields = [
'image',
]
def post(self, request, *args, **kwargs):
part = self.get_object()
form = self.get_form()
img = request.POST.get('image', '')
img = os.path.basename(img)
data = {}
if img:
img_path = os.path.join(settings.MEDIA_ROOT, 'part_images', img)
# Ensure that the image already exists
if os.path.exists(img_path):
part.image = os.path.join('part_images', img)
part.save()
data['success'] = _('Updated part image')
if 'success' not in data:
data['error'] = _('Part image not found')
return self.renderJsonResponse(request, form, data)
class PartEdit(AjaxUpdateView):
""" View for editing Part object """
model = Part
form_class = part_forms.EditPartForm
ajax_template_name = 'modal_form.html'
ajax_form_title = 'Edit Part Properties'
ajax_form_title = _('Edit Part Properties')
context_object_name = 'part'
def get_form(self):
@ -643,7 +685,7 @@ class BomValidate(AjaxUpdateView):
""" Modal form view for validating a part BOM """
model = Part
ajax_form_title = "Validate BOM"
ajax_form_title = _("Validate BOM")
ajax_template_name = 'part/bom_validate.html'
context_object_name = 'part'
form_class = part_forms.BomValidateForm
@ -1308,14 +1350,14 @@ class PartDelete(AjaxDeleteView):
model = Part
ajax_template_name = 'part/partial_delete.html'
ajax_form_title = 'Confirm Part Deletion'
ajax_form_title = _('Confirm Part Deletion')
context_object_name = 'part'
success_url = '/part/'
def get_data(self):
return {
'danger': 'Part was deleted',
'danger': _('Part was deleted'),
}
@ -1324,7 +1366,7 @@ class PartPricing(AjaxView):
model = Part
ajax_template_name = "part/part_pricing.html"
ajax_form_title = "Part Pricing"
ajax_form_title = _("Part Pricing")
form_class = part_forms.PartPriceForm
def get_part(self):
@ -1446,7 +1488,7 @@ class PartParameterTemplateCreate(AjaxCreateView):
model = PartParameterTemplate
form_class = part_forms.EditPartParameterTemplateForm
ajax_form_title = 'Create Part Parameter Template'
ajax_form_title = _('Create Part Parameter Template')
class PartParameterTemplateEdit(AjaxUpdateView):
@ -1454,14 +1496,14 @@ class PartParameterTemplateEdit(AjaxUpdateView):
model = PartParameterTemplate
form_class = part_forms.EditPartParameterTemplateForm
ajax_form_title = 'Edit Part Parameter Template'
ajax_form_title = _('Edit Part Parameter Template')
class PartParameterTemplateDelete(AjaxDeleteView):
""" View for deleting an existing PartParameterTemplate """
model = PartParameterTemplate
ajax_form_title = "Delete Part Parameter Template"
ajax_form_title = _("Delete Part Parameter Template")
class PartParameterCreate(AjaxCreateView):
@ -1469,7 +1511,7 @@ class PartParameterCreate(AjaxCreateView):
model = PartParameter
form_class = part_forms.EditPartParameterForm
ajax_form_title = 'Create Part Parameter'
ajax_form_title = _('Create Part Parameter')
def get_initial(self):
@ -1519,7 +1561,7 @@ class PartParameterEdit(AjaxUpdateView):
model = PartParameter
form_class = part_forms.EditPartParameterForm
ajax_form_title = 'Edit Part Parameter'
ajax_form_title = _('Edit Part Parameter')
def get_form(self):
@ -1533,7 +1575,7 @@ class PartParameterDelete(AjaxDeleteView):
model = PartParameter
ajax_template_name = 'part/param_delete.html'
ajax_form_title = 'Delete Part Parameter'
ajax_form_title = _('Delete Part Parameter')
class CategoryDetail(DetailView):
@ -1549,7 +1591,7 @@ class CategoryEdit(AjaxUpdateView):
model = PartCategory
form_class = part_forms.EditCategoryForm
ajax_template_name = 'modal_form.html'
ajax_form_title = 'Edit Part Category'
ajax_form_title = _('Edit Part Category')
def get_context_data(self, **kwargs):
context = super(CategoryEdit, self).get_context_data(**kwargs).copy()
@ -1584,13 +1626,13 @@ class CategoryDelete(AjaxDeleteView):
""" Delete view to delete a PartCategory """
model = PartCategory
ajax_template_name = 'part/category_delete.html'
ajax_form_title = 'Delete Part Category'
ajax_form_title = _('Delete Part Category')
context_object_name = 'category'
success_url = '/part/'
def get_data(self):
return {
'danger': 'Part category was deleted',
'danger': _('Part category was deleted'),
}
@ -1598,7 +1640,7 @@ class CategoryCreate(AjaxCreateView):
""" Create view to make a new PartCategory """
model = PartCategory
ajax_form_action = reverse_lazy('category-create')
ajax_form_title = 'Create new part category'
ajax_form_title = _('Create new part category')
ajax_template_name = 'modal_form.html'
form_class = part_forms.EditCategoryForm
@ -1649,7 +1691,7 @@ class BomItemCreate(AjaxCreateView):
model = BomItem
form_class = part_forms.EditBomItemForm
ajax_template_name = 'modal_form.html'
ajax_form_title = 'Create BOM item'
ajax_form_title = _('Create BOM item')
def get_form(self):
""" Override get_form() method to reduce Part selection options.
@ -1715,7 +1757,7 @@ class BomItemEdit(AjaxUpdateView):
model = BomItem
form_class = part_forms.EditBomItemForm
ajax_template_name = 'modal_form.html'
ajax_form_title = 'Edit BOM item'
ajax_form_title = _('Edit BOM item')
def get_form(self):
""" Override get_form() method to filter part selection options
@ -1763,4 +1805,4 @@ class BomItemDelete(AjaxDeleteView):
model = BomItem
ajax_template_name = 'part/bom-delete.html'
context_object_name = 'item'
ajax_form_title = 'Confim BOM item deletion'
ajax_form_title = _('Confim BOM item deletion')

View File

@ -44,7 +44,6 @@ class CreateStockItemForm(HelperForm):
'serial_numbers',
'delete_on_deplete',
'status',
'notes',
'URL',
]

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.9 on 2020-02-06 12:13
from django.db import migrations
import markdownx.models
class Migration(migrations.Migration):
dependencies = [
('stock', '0019_auto_20200202_1024'),
]
operations = [
migrations.AlterField(
model_name='stockitem',
name='notes',
field=markdownx.models.MarkdownxField(blank=True, help_text='Stock Item Notes', null=True),
),
]

View File

@ -360,7 +360,7 @@ class StockItem(models.Model):
choices=StockStatus.items(),
validators=[MinValueValidator(0)])
notes = MarkdownxField(blank=True, help_text=_('Stock Item Notes'))
notes = MarkdownxField(blank=True, null=True, help_text=_('Stock Item Notes'))
# If stock item is incoming, an (optional) ETA field
# expected_arrival = models.DateField(null=True, blank=True)

View File

@ -884,34 +884,49 @@ class StockItemCreate(AjaxCreateView):
form.errors['serial_numbers'] = [_('The following serial numbers already exist: ({sn})'.format(sn=exists))]
valid = False
# At this point we have a list of serial numbers which we know are valid,
# and do not currently exist
form.clean()
else:
# At this point we have a list of serial numbers which we know are valid,
# and do not currently exist
form.clean()
data = form.cleaned_data
form_data = form.cleaned_data
for serial in serials:
# Create a new stock item for each serial number
item = StockItem(
part=part,
quantity=1,
serial=serial,
supplier_part=data.get('supplier_part'),
location=data.get('location'),
batch=data.get('batch'),
delete_on_deplete=False,
status=data.get('status'),
notes=data.get('notes'),
URL=data.get('URL'),
)
for serial in serials:
# Create a new stock item for each serial number
item = StockItem(
part=part,
quantity=1,
serial=serial,
supplier_part=form_data.get('supplier_part'),
location=form_data.get('location'),
batch=form_data.get('batch'),
delete_on_deplete=False,
status=form_data.get('status'),
URL=form_data.get('URL'),
)
item.save(user=request.user)
item.save(user=request.user)
data['success'] = _('Created {n} new stock items'.format(n=len(serials)))
valid = True
except ValidationError as e:
form.errors['serial_numbers'] = e.messages
valid = False
else:
else:
# We have a serialized part, but no serial numbers specified...
form.clean()
form._post_clean()
item = form.save(commit=False)
item.save(user=request.user)
data['pk'] = item.pk
data['url'] = item.get_absolute_url()
data['success'] = _("Created new stock item")
else: # Referenced Part object is not marked as "trackable"
# For non-serialized items, simply save the form.
# We need to call _post_clean() here because it is prevented in the form implementation
form.clean()