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/' MEDIA_URL = '/media/'
# The filesystem location for served static files # 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 forms use the bootstrap templates
CRISPY_TEMPLATE_PACK = 'bootstrap' CRISPY_TEMPLATE_PACK = 'bootstrap'

View File

@ -183,6 +183,24 @@
-webkit-opacity: 10%; -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 { .btn-glyph {
padding-left: 6px; padding-left: 6px;
@ -211,6 +229,20 @@
object-fit: contain; 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 { .checkbox {
margin-left: 20px; 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, batch = models.CharField(max_length=100, blank=True, null=True,
help_text=_('Batch code for this build output')) 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) completion_date = models.DateField(null=True, blank=True)

View File

@ -6,7 +6,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2020-02-02 08:07+0100\n"
"Last-Translator: Christian Schlüter <chschlue@gmail.com>\n" "Last-Translator: Christian Schlüter <chschlue@gmail.com>\n"
"Language-Team: C <kde-i18n-doc@kde.org>\n" "Language-Team: C <kde-i18n-doc@kde.org>\n"
@ -706,7 +706,7 @@ msgstr "Link auf externe Seite"
msgid "Order notes" msgid "Order notes"
msgstr "Bestell-Notizen" 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 #: stock/models.py:440
msgid "Quantity must be greater than zero" msgid "Quantity must be greater than zero"
msgstr "Anzahl muss größer Null sein" msgstr "Anzahl muss größer Null sein"
@ -1277,56 +1277,208 @@ msgstr "Tracking"
msgid "Attachments" msgid "Attachments"
msgstr "Anhänge" 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 #: part/views.py:196
#, python-brace-format #, python-brace-format
msgid "Set category for {n} parts" msgid "Set category for {n} parts"
msgstr "Kategorie für {n} Teile setzen" 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" msgid "No BOM file provided"
msgstr "Keine Stückliste angegeben" msgstr "Keine Stückliste angegeben"
#: part/views.py:1069 #: part/views.py:1082
msgid "Enter a valid quantity" msgid "Enter a valid quantity"
msgstr "Bitte eine gültige Anzahl eingeben" 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" msgid "Select valid part"
msgstr "Bitte ein gültiges Teil auswählen" msgstr "Bitte ein gültiges Teil auswählen"
#: part/views.py:1102 #: part/views.py:1115
msgid "Duplicate part selected" msgid "Duplicate part selected"
msgstr "Teil doppelt ausgewählt" msgstr "Teil doppelt ausgewählt"
#: part/views.py:1130 #: part/views.py:1143
msgid "Select a part" msgid "Select a part"
msgstr "Teil auswählen" msgstr "Teil auswählen"
#: part/views.py:1134 #: part/views.py:1147
msgid "Specify quantity" msgid "Specify quantity"
msgstr "Anzahl angeben" 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" msgid "File Format"
msgstr "Dateiformat" msgstr "Dateiformat"
#: stock/forms.py:92 #: stock/forms.py:91
msgid "Select output file format" msgid "Select output file format"
msgstr "Ausgabe-Dateiformat auswählen" msgstr "Ausgabe-Dateiformat auswählen"
#: stock/forms.py:94 #: stock/forms.py:93
msgid "Include stock items in sub locations" msgid "Include stock items in sub locations"
msgstr "Lagerobjekte in untergeordneten Lagerorten einschließen" msgstr "Lagerobjekte in untergeordneten Lagerorten einschließen"
#: stock/forms.py:127 #: stock/forms.py:126
msgid "Destination stock location" msgid "Destination stock location"
msgstr "Ziel-Lagerbestand" msgstr "Ziel-Lagerbestand"
#: stock/forms.py:133 #: stock/forms.py:132
msgid "Confirm movement of stock items" msgid "Confirm movement of stock items"
msgstr "Bewegung der Lagerobjekte bestätigen" 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" msgid "Set the destination as the default location for selected parts"
msgstr "Setze das Ziel als Standard-Ziel für ausgewählte Teile" msgstr "Setze das Ziel als Standard-Ziel für ausgewählte Teile"
@ -1652,27 +1804,33 @@ msgstr "Ungültige Menge"
msgid "Invalid part selection" msgid "Invalid part selection"
msgstr "Ungültige Teileauswahl" 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" msgid "Created new stock item"
msgstr "Neues Lagerobjekt erstellt" msgstr "Neues Lagerobjekt erstellt"
#: stock/views.py:942 #: stock/views.py:957
msgid "Delete Stock Location" msgid "Delete Stock Location"
msgstr "Standort löschen" msgstr "Standort löschen"
#: stock/views.py:955 #: stock/views.py:970
msgid "Delete Stock Item" msgid "Delete Stock Item"
msgstr "Lagerobjekt löschen" msgstr "Lagerobjekt löschen"
#: stock/views.py:966 #: stock/views.py:981
msgid "Delete Stock Tracking Entry" msgid "Delete Stock Tracking Entry"
msgstr "Lagerbestands-Tracking-Eintrag löschen" msgstr "Lagerbestands-Tracking-Eintrag löschen"
#: stock/views.py:983 #: stock/views.py:998
msgid "Edit Stock Tracking Entry" msgid "Edit Stock Tracking Entry"
msgstr "Lagerbestands-Tracking-Eintrag bearbeiten" msgstr "Lagerbestands-Tracking-Eintrag bearbeiten"
#: stock/views.py:992 #: stock/views.py:1007
msgid "Add Stock Tracking Entry" msgid "Add Stock Tracking Entry"
msgstr "Lagerbestands-Tracking-Eintrag hinzufügen" msgstr "Lagerbestands-Tracking-Eintrag hinzufügen"

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -675,7 +675,7 @@ msgstr ""
msgid "Order notes" msgid "Order notes"
msgstr "" 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 #: stock/models.py:440
msgid "Quantity must be greater than zero" msgid "Quantity must be greater than zero"
msgstr "" msgstr ""
@ -1242,56 +1242,164 @@ msgstr ""
msgid "Attachments" msgid "Attachments"
msgstr "" msgstr ""
#: part/views.py:77
msgid "Added attachment"
msgstr ""
#: part/views.py:119
msgid "Part attachment updated"
msgstr ""
#: part/views.py:196 #: part/views.py:196
#, python-brace-format #, python-brace-format
msgid "Set category for {n} parts" msgid "Set category for {n} parts"
msgstr "" 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" msgid "No BOM file provided"
msgstr "" msgstr ""
#: part/views.py:1069 #: part/views.py:1082
msgid "Enter a valid quantity" msgid "Enter a valid quantity"
msgstr "" msgstr ""
#: part/views.py:1093 part/views.py:1096 #: part/views.py:1106 part/views.py:1109
msgid "Select valid part" msgid "Select valid part"
msgstr "" msgstr ""
#: part/views.py:1102 #: part/views.py:1115
msgid "Duplicate part selected" msgid "Duplicate part selected"
msgstr "" msgstr ""
#: part/views.py:1130 #: part/views.py:1143
msgid "Select a part" msgid "Select a part"
msgstr "" msgstr ""
#: part/views.py:1134 #: part/views.py:1147
msgid "Specify quantity" msgid "Specify quantity"
msgstr "" 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" msgid "File Format"
msgstr "" msgstr ""
#: stock/forms.py:92 #: stock/forms.py:91
msgid "Select output file format" msgid "Select output file format"
msgstr "" msgstr ""
#: stock/forms.py:94 #: stock/forms.py:93
msgid "Include stock items in sub locations" msgid "Include stock items in sub locations"
msgstr "" msgstr ""
#: stock/forms.py:127 #: stock/forms.py:126
msgid "Destination stock location" msgid "Destination stock location"
msgstr "" msgstr ""
#: stock/forms.py:133 #: stock/forms.py:132
msgid "Confirm movement of stock items" msgid "Confirm movement of stock items"
msgstr "" msgstr ""
#: stock/forms.py:135 #: stock/forms.py:134
msgid "Set the destination as the default location for selected parts" msgid "Set the destination as the default location for selected parts"
msgstr "" msgstr ""
@ -1610,27 +1718,32 @@ msgstr ""
msgid "Invalid part selection" msgid "Invalid part selection"
msgstr "" 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" msgid "Created new stock item"
msgstr "" msgstr ""
#: stock/views.py:942 #: stock/views.py:957
msgid "Delete Stock Location" msgid "Delete Stock Location"
msgstr "" msgstr ""
#: stock/views.py:955 #: stock/views.py:970
msgid "Delete Stock Item" msgid "Delete Stock Item"
msgstr "" msgstr ""
#: stock/views.py:966 #: stock/views.py:981
msgid "Delete Stock Tracking Entry" msgid "Delete Stock Tracking Entry"
msgstr "" msgstr ""
#: stock/views.py:983 #: stock/views.py:998
msgid "Edit Stock Tracking Entry" msgid "Edit Stock Tracking Entry"
msgstr "" msgstr ""
#: stock/views.py:992 #: stock/views.py:1007
msgid "Add Stock Tracking Entry" msgid "Add Stock Tracking Entry"
msgstr "" msgstr ""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -675,7 +675,7 @@ msgstr ""
msgid "Order notes" msgid "Order notes"
msgstr "" 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 #: stock/models.py:440
msgid "Quantity must be greater than zero" msgid "Quantity must be greater than zero"
msgstr "" msgstr ""
@ -1242,56 +1242,164 @@ msgstr ""
msgid "Attachments" msgid "Attachments"
msgstr "" msgstr ""
#: part/views.py:77
msgid "Added attachment"
msgstr ""
#: part/views.py:119
msgid "Part attachment updated"
msgstr ""
#: part/views.py:196 #: part/views.py:196
#, python-brace-format #, python-brace-format
msgid "Set category for {n} parts" msgid "Set category for {n} parts"
msgstr "" 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" msgid "No BOM file provided"
msgstr "" msgstr ""
#: part/views.py:1069 #: part/views.py:1082
msgid "Enter a valid quantity" msgid "Enter a valid quantity"
msgstr "" msgstr ""
#: part/views.py:1093 part/views.py:1096 #: part/views.py:1106 part/views.py:1109
msgid "Select valid part" msgid "Select valid part"
msgstr "" msgstr ""
#: part/views.py:1102 #: part/views.py:1115
msgid "Duplicate part selected" msgid "Duplicate part selected"
msgstr "" msgstr ""
#: part/views.py:1130 #: part/views.py:1143
msgid "Select a part" msgid "Select a part"
msgstr "" msgstr ""
#: part/views.py:1134 #: part/views.py:1147
msgid "Specify quantity" msgid "Specify quantity"
msgstr "" 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" msgid "File Format"
msgstr "" msgstr ""
#: stock/forms.py:92 #: stock/forms.py:91
msgid "Select output file format" msgid "Select output file format"
msgstr "" msgstr ""
#: stock/forms.py:94 #: stock/forms.py:93
msgid "Include stock items in sub locations" msgid "Include stock items in sub locations"
msgstr "" msgstr ""
#: stock/forms.py:127 #: stock/forms.py:126
msgid "Destination stock location" msgid "Destination stock location"
msgstr "" msgstr ""
#: stock/forms.py:133 #: stock/forms.py:132
msgid "Confirm movement of stock items" msgid "Confirm movement of stock items"
msgstr "" msgstr ""
#: stock/forms.py:135 #: stock/forms.py:134
msgid "Set the destination as the default location for selected parts" msgid "Set the destination as the default location for selected parts"
msgstr "" msgstr ""
@ -1610,27 +1718,32 @@ msgstr ""
msgid "Invalid part selection" msgid "Invalid part selection"
msgstr "" 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" msgid "Created new stock item"
msgstr "" msgstr ""
#: stock/views.py:942 #: stock/views.py:957
msgid "Delete Stock Location" msgid "Delete Stock Location"
msgstr "" msgstr ""
#: stock/views.py:955 #: stock/views.py:970
msgid "Delete Stock Item" msgid "Delete Stock Item"
msgstr "" msgstr ""
#: stock/views.py:966 #: stock/views.py:981
msgid "Delete Stock Tracking Entry" msgid "Delete Stock Tracking Entry"
msgstr "" msgstr ""
#: stock/views.py:983 #: stock/views.py:998
msgid "Edit Stock Tracking Entry" msgid "Edit Stock Tracking Entry"
msgstr "" msgstr ""
#: stock/views.py:992 #: stock/views.py:1007
msgid "Add Stock Tracking Entry" msgid "Add Stock Tracking Entry"
msgstr "" msgstr ""

View File

@ -8,7 +8,7 @@ from __future__ import unicode_literals
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from django.conf import settings 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 import status
from rest_framework.response import Response from rest_framework.response import Response
@ -23,10 +23,7 @@ import os
from .models import Part, PartCategory, BomItem, PartStar from .models import Part, PartCategory, BomItem, PartStar
from .models import PartParameter, PartParameterTemplate from .models import PartParameter, PartParameterTemplate
from .serializers import PartSerializer, BomItemSerializer from . import serializers as part_serializers
from .serializers import CategorySerializer
from .serializers import PartStarSerializer
from .serializers import PartParameterSerializer, PartParameterTemplateSerializer
from InvenTree.views import TreeSerializer from InvenTree.views import TreeSerializer
from InvenTree.helpers import str2bool from InvenTree.helpers import str2bool
@ -53,7 +50,7 @@ class CategoryList(generics.ListCreateAPIView):
""" """
queryset = PartCategory.objects.all() queryset = PartCategory.objects.all()
serializer_class = CategorySerializer serializer_class = part_serializers.CategorySerializer
permission_classes = [ permission_classes = [
permissions.IsAuthenticated, permissions.IsAuthenticated,
@ -83,14 +80,37 @@ class CategoryList(generics.ListCreateAPIView):
class CategoryDetail(generics.RetrieveUpdateDestroyAPIView): class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
""" API endpoint for detail view of a single PartCategory object """ """ API endpoint for detail view of a single PartCategory object """
serializer_class = CategorySerializer serializer_class = part_serializers.CategorySerializer
queryset = PartCategory.objects.all() 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): class PartDetail(generics.RetrieveUpdateAPIView):
""" API endpoint for detail view of a single Part object """ """ API endpoint for detail view of a single Part object """
queryset = Part.objects.all() queryset = Part.objects.all()
serializer_class = PartSerializer serializer_class = part_serializers.PartSerializer
permission_classes = [ permission_classes = [
permissions.IsAuthenticated, permissions.IsAuthenticated,
@ -104,12 +124,12 @@ class PartList(generics.ListCreateAPIView):
- POST: Create a new Part object - POST: Create a new Part object
""" """
serializer_class = PartSerializer serializer_class = part_serializers.PartSerializer
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
""" """
Instead of using the DRF serialiser to LIST, 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. This turns out to be significantly faster.
""" """
@ -218,7 +238,7 @@ class PartStarDetail(generics.RetrieveDestroyAPIView):
""" API endpoint for viewing or removing a PartStar object """ """ API endpoint for viewing or removing a PartStar object """
queryset = PartStar.objects.all() queryset = PartStar.objects.all()
serializer_class = PartStarSerializer serializer_class = part_serializers.PartStarSerializer
class PartStarList(generics.ListCreateAPIView): class PartStarList(generics.ListCreateAPIView):
@ -229,7 +249,7 @@ class PartStarList(generics.ListCreateAPIView):
""" """
queryset = PartStar.objects.all() queryset = PartStar.objects.all()
serializer_class = PartStarSerializer serializer_class = part_serializers.PartStarSerializer
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
@ -271,7 +291,7 @@ class PartParameterTemplateList(generics.ListCreateAPIView):
""" """
queryset = PartParameterTemplate.objects.all() queryset = PartParameterTemplate.objects.all()
serializer_class = PartParameterTemplateSerializer serializer_class = part_serializers.PartParameterTemplateSerializer
permission_classes = [ permission_classes = [
permissions.IsAuthenticated, permissions.IsAuthenticated,
@ -294,7 +314,7 @@ class PartParameterList(generics.ListCreateAPIView):
""" """
queryset = PartParameter.objects.all() queryset = PartParameter.objects.all()
serializer_class = PartParameterSerializer serializer_class = part_serializers.PartParameterSerializer
permission_classes = [ permission_classes = [
permissions.IsAuthenticated, permissions.IsAuthenticated,
@ -317,7 +337,7 @@ class BomList(generics.ListCreateAPIView):
- POST: Create a new BomItem object - POST: Create a new BomItem object
""" """
serializer_class = BomItemSerializer serializer_class = part_serializers.BomItemSerializer
def get_serializer(self, *args, **kwargs): def get_serializer(self, *args, **kwargs):
@ -360,7 +380,7 @@ class BomDetail(generics.RetrieveUpdateDestroyAPIView):
""" API endpoint for detail view of a single BomItem object """ """ API endpoint for detail view of a single BomItem object """
queryset = BomItem.objects.all() queryset = BomItem.objects.all()
serializer_class = BomItemSerializer serializer_class = part_serializers.BomItemSerializer
permission_classes = [ permission_classes = [
permissions.IsAuthenticated, permissions.IsAuthenticated,
@ -424,6 +444,8 @@ part_api_urls = [
url(r'^star/', include(part_star_api_urls)), url(r'^star/', include(part_star_api_urls)),
url(r'^parameter/', include(part_param_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'^(?P<pk>\d+)/?', PartDetail.as_view(), name='api-part-detail'),
url(r'^.*$', PartList.as_view(), name='api-part-list'), 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.urls import reverse
from django.conf import settings from django.conf import settings
from django.core.files.base import ContentFile
from django.db import models, transaction from django.db import models, transaction
from django.db.models import Sum from django.db.models import Sum
from django.db.models import prefetch_related_objects from django.db.models import prefetch_related_objects
@ -24,6 +23,8 @@ from django.dispatch import receiver
from markdownx.models import MarkdownxField from markdownx.models import MarkdownxField
from django_cleanup import cleanup
from mptt.models import TreeForeignKey from mptt.models import TreeForeignKey
from datetime import datetime from datetime import datetime
@ -136,18 +137,9 @@ def rename_part_image(instance, filename):
""" """
base = 'part_images' base = 'part_images'
fname = os.path.basename(filename)
if filename.count('.') > 0: return os.path.join(base, fname)
ext = filename.split('.')[-1]
else:
ext = ''
fn = 'part_{pk}_img'.format(pk=instance.pk)
if ext:
fn += '.' + ext
return os.path.join(base, fn)
def match_part_names(match, threshold=80, reverse=True, compare_length=False): 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 return matches
@cleanup.ignore
class Part(models.Model): class Part(models.Model):
""" The Part object represents an abstract part, the 'concept' of an actual entity. """ 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 = "Part"
verbose_name_plural = "Parts" 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): def __str__(self):
return "{n} - {d}".format(n=self.full_name, d=self.description) return "{n} - {d}".format(n=self.full_name, d=self.description)
@ -832,10 +845,8 @@ class Part(models.Model):
# Copy the part image # Copy the part image
if kwargs.get('image', True): if kwargs.get('image', True):
if other.image: if other.image:
image_file = ContentFile(other.image.read()) # Reference the other image from this Part
image_file.name = rename_part_image(self, other.image.url) self.image = other.image
self.image = image_file
# Copy the BOM data # Copy the BOM data
if kwargs.get('bom', False): 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): class PartBriefSerializer(InvenTreeModelSerializer):
""" Serializer for Part (brief detail) """ """ Serializer for Part (brief detail) """

View File

@ -25,17 +25,7 @@
<div class="row"> <div class="row">
<div class="col-sm-6"> <div class="col-sm-6">
<div class="media"> {% include "part/part_thumb.html" %}
<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>
<div class="media-body"> <div class="media-body">
<h4> <h4>
{{ part.full_name }} {{ part.full_name }}
@ -163,7 +153,7 @@
enableDragAndDrop( enableDragAndDrop(
'#part-thumb', '#part-thumb',
"{% url 'part-image' part.id %}", "{% url 'part-image-upload' part.id %}",
{ {
label: 'image', label: 'image',
success: function(data, status, xhr) { success: function(data, status, xhr) {
@ -208,13 +198,54 @@
}); });
}); });
$("#part-thumb").click(function() { $("#part-image-upload").click(function() {
launchModalForm( launchModalForm("{% url 'part-image-upload' part.id %}",
"{% url 'part-image' 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() { $("#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): def test_hash(self):
hash = inventree_extras.inventree_commit_hash() hash = inventree_extras.inventree_commit_hash()
self.assertEqual(len(hash), 7) self.assertGreater(len(hash), 5)
def test_date(self): def test_date(self):
d = inventree_extras.inventree_commit_date() d = inventree_extras.inventree_commit_date()
@ -68,11 +68,8 @@ class PartTest(TestCase):
def test_rename_img(self): def test_rename_img(self):
img = rename_part_image(self.R1, 'hello.png') img = rename_part_image(self.R1, 'hello.png')
self.assertEqual(img, os.path.join('part_images', 'part_3_img.png')) self.assertEqual(img, os.path.join('part_images', 'hello.png'))
img = rename_part_image(self.R2, 'test')
self.assertEqual(img, os.path.join('part_images', 'part_4_img'))
def test_stock(self): def test_stock(self):
# No stock of any resistors # No stock of any resistors
res = Part.objects.filter(description__contains='resistor') 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'), url(r'^qr_code/?', views.PartQRCode.as_view(), name='part-qr'),
# Normal thumbnail with form # 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 # Any other URLs go to the part detail page
url(r'^.*$', views.PartDetail.as_view(), name='part-detail'), 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.views.generic import DetailView, ListView, FormView, UpdateView
from django.forms.models import model_to_dict from django.forms.models import model_to_dict
from django.forms import HiddenInput, CheckboxInput from django.forms import HiddenInput, CheckboxInput
from django.conf import settings
import os
from fuzzywuzzy import fuzz from fuzzywuzzy import fuzz
from decimal import Decimal from decimal import Decimal
@ -74,7 +77,7 @@ class PartAttachmentCreate(AjaxCreateView):
def get_data(self): def get_data(self):
return { return {
'success': 'Added attachment' 'success': _('Added attachment')
} }
def get_initial(self): def get_initial(self):
@ -116,7 +119,7 @@ class PartAttachmentEdit(AjaxUpdateView):
def get_data(self): def get_data(self):
return { return {
'success': 'Part attachment updated' 'success': _('Part attachment updated')
} }
def get_form(self): def get_form(self):
@ -303,7 +306,7 @@ class PartDuplicate(AjaxCreateView):
def get_data(self): def get_data(self):
return { return {
'success': 'Copied part' 'success': _('Copied part')
} }
def get_part_to_copy(self): def get_part_to_copy(self):
@ -411,12 +414,12 @@ class PartCreate(AjaxCreateView):
model = Part model = Part
form_class = part_forms.EditPartForm form_class = part_forms.EditPartForm
ajax_form_title = 'Create new part' ajax_form_title = _('Create new part')
ajax_template_name = 'part/create_part.html' ajax_template_name = 'part/create_part.html'
def get_data(self): def get_data(self):
return { return {
'success': "Created new part", 'success': _("Created new part"),
} }
def get_category_id(self): def get_category_id(self):
@ -601,27 +604,66 @@ class PartQRCode(QRCodeView):
return None return None
class PartImage(AjaxUpdateView): class PartImageUpload(AjaxUpdateView):
""" View for uploading Part image """ """ View for uploading a new Part image """
model = Part model = Part
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
ajax_form_title = 'Upload Part Image' ajax_form_title = _('Upload Part Image')
form_class = part_forms.PartImageForm form_class = part_forms.PartImageForm
def get_data(self): def get_data(self):
return { 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): class PartEdit(AjaxUpdateView):
""" View for editing Part object """ """ View for editing Part object """
model = Part model = Part
form_class = part_forms.EditPartForm form_class = part_forms.EditPartForm
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
ajax_form_title = 'Edit Part Properties' ajax_form_title = _('Edit Part Properties')
context_object_name = 'part' context_object_name = 'part'
def get_form(self): def get_form(self):
@ -643,7 +685,7 @@ class BomValidate(AjaxUpdateView):
""" Modal form view for validating a part BOM """ """ Modal form view for validating a part BOM """
model = Part model = Part
ajax_form_title = "Validate BOM" ajax_form_title = _("Validate BOM")
ajax_template_name = 'part/bom_validate.html' ajax_template_name = 'part/bom_validate.html'
context_object_name = 'part' context_object_name = 'part'
form_class = part_forms.BomValidateForm form_class = part_forms.BomValidateForm
@ -1308,14 +1350,14 @@ class PartDelete(AjaxDeleteView):
model = Part model = Part
ajax_template_name = 'part/partial_delete.html' ajax_template_name = 'part/partial_delete.html'
ajax_form_title = 'Confirm Part Deletion' ajax_form_title = _('Confirm Part Deletion')
context_object_name = 'part' context_object_name = 'part'
success_url = '/part/' success_url = '/part/'
def get_data(self): def get_data(self):
return { return {
'danger': 'Part was deleted', 'danger': _('Part was deleted'),
} }
@ -1324,7 +1366,7 @@ class PartPricing(AjaxView):
model = Part model = Part
ajax_template_name = "part/part_pricing.html" ajax_template_name = "part/part_pricing.html"
ajax_form_title = "Part Pricing" ajax_form_title = _("Part Pricing")
form_class = part_forms.PartPriceForm form_class = part_forms.PartPriceForm
def get_part(self): def get_part(self):
@ -1446,7 +1488,7 @@ class PartParameterTemplateCreate(AjaxCreateView):
model = PartParameterTemplate model = PartParameterTemplate
form_class = part_forms.EditPartParameterTemplateForm form_class = part_forms.EditPartParameterTemplateForm
ajax_form_title = 'Create Part Parameter Template' ajax_form_title = _('Create Part Parameter Template')
class PartParameterTemplateEdit(AjaxUpdateView): class PartParameterTemplateEdit(AjaxUpdateView):
@ -1454,14 +1496,14 @@ class PartParameterTemplateEdit(AjaxUpdateView):
model = PartParameterTemplate model = PartParameterTemplate
form_class = part_forms.EditPartParameterTemplateForm form_class = part_forms.EditPartParameterTemplateForm
ajax_form_title = 'Edit Part Parameter Template' ajax_form_title = _('Edit Part Parameter Template')
class PartParameterTemplateDelete(AjaxDeleteView): class PartParameterTemplateDelete(AjaxDeleteView):
""" View for deleting an existing PartParameterTemplate """ """ View for deleting an existing PartParameterTemplate """
model = PartParameterTemplate model = PartParameterTemplate
ajax_form_title = "Delete Part Parameter Template" ajax_form_title = _("Delete Part Parameter Template")
class PartParameterCreate(AjaxCreateView): class PartParameterCreate(AjaxCreateView):
@ -1469,7 +1511,7 @@ class PartParameterCreate(AjaxCreateView):
model = PartParameter model = PartParameter
form_class = part_forms.EditPartParameterForm form_class = part_forms.EditPartParameterForm
ajax_form_title = 'Create Part Parameter' ajax_form_title = _('Create Part Parameter')
def get_initial(self): def get_initial(self):
@ -1519,7 +1561,7 @@ class PartParameterEdit(AjaxUpdateView):
model = PartParameter model = PartParameter
form_class = part_forms.EditPartParameterForm form_class = part_forms.EditPartParameterForm
ajax_form_title = 'Edit Part Parameter' ajax_form_title = _('Edit Part Parameter')
def get_form(self): def get_form(self):
@ -1533,7 +1575,7 @@ class PartParameterDelete(AjaxDeleteView):
model = PartParameter model = PartParameter
ajax_template_name = 'part/param_delete.html' ajax_template_name = 'part/param_delete.html'
ajax_form_title = 'Delete Part Parameter' ajax_form_title = _('Delete Part Parameter')
class CategoryDetail(DetailView): class CategoryDetail(DetailView):
@ -1549,7 +1591,7 @@ class CategoryEdit(AjaxUpdateView):
model = PartCategory model = PartCategory
form_class = part_forms.EditCategoryForm form_class = part_forms.EditCategoryForm
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
ajax_form_title = 'Edit Part Category' ajax_form_title = _('Edit Part Category')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(CategoryEdit, self).get_context_data(**kwargs).copy() context = super(CategoryEdit, self).get_context_data(**kwargs).copy()
@ -1584,13 +1626,13 @@ class CategoryDelete(AjaxDeleteView):
""" Delete view to delete a PartCategory """ """ Delete view to delete a PartCategory """
model = PartCategory model = PartCategory
ajax_template_name = 'part/category_delete.html' ajax_template_name = 'part/category_delete.html'
ajax_form_title = 'Delete Part Category' ajax_form_title = _('Delete Part Category')
context_object_name = 'category' context_object_name = 'category'
success_url = '/part/' success_url = '/part/'
def get_data(self): def get_data(self):
return { 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 """ """ Create view to make a new PartCategory """
model = PartCategory model = PartCategory
ajax_form_action = reverse_lazy('category-create') 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' ajax_template_name = 'modal_form.html'
form_class = part_forms.EditCategoryForm form_class = part_forms.EditCategoryForm
@ -1649,7 +1691,7 @@ class BomItemCreate(AjaxCreateView):
model = BomItem model = BomItem
form_class = part_forms.EditBomItemForm form_class = part_forms.EditBomItemForm
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
ajax_form_title = 'Create BOM item' ajax_form_title = _('Create BOM item')
def get_form(self): def get_form(self):
""" Override get_form() method to reduce Part selection options. """ Override get_form() method to reduce Part selection options.
@ -1715,7 +1757,7 @@ class BomItemEdit(AjaxUpdateView):
model = BomItem model = BomItem
form_class = part_forms.EditBomItemForm form_class = part_forms.EditBomItemForm
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
ajax_form_title = 'Edit BOM item' ajax_form_title = _('Edit BOM item')
def get_form(self): def get_form(self):
""" Override get_form() method to filter part selection options """ Override get_form() method to filter part selection options
@ -1763,4 +1805,4 @@ class BomItemDelete(AjaxDeleteView):
model = BomItem model = BomItem
ajax_template_name = 'part/bom-delete.html' ajax_template_name = 'part/bom-delete.html'
context_object_name = 'item' 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', 'serial_numbers',
'delete_on_deplete', 'delete_on_deplete',
'status', 'status',
'notes',
'URL', '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(), choices=StockStatus.items(),
validators=[MinValueValidator(0)]) 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 # If stock item is incoming, an (optional) ETA field
# expected_arrival = models.DateField(null=True, blank=True) # 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))] form.errors['serial_numbers'] = [_('The following serial numbers already exist: ({sn})'.format(sn=exists))]
valid = False valid = False
# At this point we have a list of serial numbers which we know are valid, else:
# and do not currently exist # At this point we have a list of serial numbers which we know are valid,
form.clean() # and do not currently exist
form.clean()
data = form.cleaned_data form_data = form.cleaned_data
for serial in serials: for serial in serials:
# Create a new stock item for each serial number # Create a new stock item for each serial number
item = StockItem( item = StockItem(
part=part, part=part,
quantity=1, quantity=1,
serial=serial, serial=serial,
supplier_part=data.get('supplier_part'), supplier_part=form_data.get('supplier_part'),
location=data.get('location'), location=form_data.get('location'),
batch=data.get('batch'), batch=form_data.get('batch'),
delete_on_deplete=False, delete_on_deplete=False,
status=data.get('status'), status=form_data.get('status'),
notes=data.get('notes'), URL=form_data.get('URL'),
URL=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: except ValidationError as e:
form.errors['serial_numbers'] = e.messages form.errors['serial_numbers'] = e.messages
valid = False 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. # For non-serialized items, simply save the form.
# We need to call _post_clean() here because it is prevented in the form implementation # We need to call _post_clean() here because it is prevented in the form implementation
form.clean() form.clean()