From ac3dcac64167700ea701c898117aaf71171d57a4 Mon Sep 17 00:00:00 2001
From: eeintech
Date: Mon, 2 Aug 2021 15:05:24 -0400
Subject: [PATCH 01/28] Re-enabled installing stock items into others
---
.../build/templates/build/build_base.html | 4 +-
InvenTree/stock/forms.py | 23 +++--
InvenTree/stock/templates/stock/item.html | 19 ++++
.../stock/templates/stock/item_base.html | 24 ++++-
.../stock/templates/stock/item_install.html | 22 ++++-
InvenTree/stock/views.py | 92 +++++++++++++++----
6 files changed, 146 insertions(+), 38 deletions(-)
diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html
index 5770777d28..e3119e6fdb 100644
--- a/InvenTree/build/templates/build/build_base.html
+++ b/InvenTree/build/templates/build/build_base.html
@@ -111,8 +111,8 @@ src="{% static 'img/blank_image.png' %}"
{% trans "Cancel Build" %}
{% endif %}
{% if build.status == BuildStatus.CANCELLED and roles.build.delete %}
- {% trans "Delete Build"% }
- {% endif %}
+ {% trans "Delete Build" %}
+ {% endif %}
{% endif %}
diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py
index b23b71a2d6..b088a6cdef 100644
--- a/InvenTree/stock/forms.py
+++ b/InvenTree/stock/forms.py
@@ -241,16 +241,21 @@ class InstallStockForm(HelperForm):
help_text=_('Stock item to install')
)
- quantity_to_install = RoundingDecimalFormField(
- max_digits=10, decimal_places=5,
- initial=1,
- label=_('Quantity'),
- help_text=_('Stock quantity to assign'),
- validators=[
- MinValueValidator(0.001)
- ]
+ to_install = forms.BooleanField(
+ widget=forms.HiddenInput(),
+ required=False
)
+ # quantity_to_install = RoundingDecimalFormField(
+ # max_digits=10, decimal_places=5,
+ # initial=1,
+ # label=_('Quantity'),
+ # help_text=_('Stock quantity to assign'),
+ # validators=[
+ # MinValueValidator(0.001)
+ # ]
+ # )
+
notes = forms.CharField(
required=False,
help_text=_('Notes')
@@ -261,7 +266,7 @@ class InstallStockForm(HelperForm):
fields = [
'part',
'stock_item',
- 'quantity_to_install',
+ # 'quantity_to_install',
'notes',
]
diff --git a/InvenTree/stock/templates/stock/item.html b/InvenTree/stock/templates/stock/item.html
index 8a00c1c5e6..d380ea3369 100644
--- a/InvenTree/stock/templates/stock/item.html
+++ b/InvenTree/stock/templates/stock/item.html
@@ -119,6 +119,11 @@
{% trans "Installed Stock Items" %}
+
+
+
@@ -128,6 +133,20 @@
{% block js_ready %}
{{ block.super }}
+ $('#stock-item-install').click(function() {
+
+ launchModalForm(
+ "{% url 'stock-item-install' item.pk %}",
+ {
+ data: {
+ 'part': {{ item.part.pk }},
+ 'install_item': true,
+ },
+ reload: true,
+ }
+ );
+ });
+
loadInstalledInTable(
$('#installed-table'),
{
diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html
index b16d9b0b1a..93698e3d05 100644
--- a/InvenTree/stock/templates/stock/item_base.html
+++ b/InvenTree/stock/templates/stock/item_base.html
@@ -127,9 +127,11 @@
{% trans "Return to stock" %}
{% endif %}
{% if item.belongs_to %}
-
- {% trans "Uninstall" %}
-
+ {% trans "Uninstall" %}
+ {% else %}
+ {% if item.part.get_used_in %}
+ {% trans "Install" %}
+ {% endif %}
{% endif %}
@@ -461,13 +463,27 @@ $("#stock-serialize").click(function() {
);
});
+$('#stock-install-in').click(function() {
+
+ launchModalForm(
+ "{% url 'stock-item-install' item.pk %}",
+ {
+ data: {
+ 'part': {{ item.part.pk }},
+ 'install_in': true,
+ },
+ reload: true,
+ }
+ );
+});
+
$('#stock-uninstall').click(function() {
launchModalForm(
"{% url 'stock-item-uninstall' %}",
{
data: {
- 'items[]': [{{ item.pk}}],
+ 'items[]': [{{ item.pk }}],
},
reload: true,
}
diff --git a/InvenTree/stock/templates/stock/item_install.html b/InvenTree/stock/templates/stock/item_install.html
index 04798972d2..8a94f304d3 100644
--- a/InvenTree/stock/templates/stock/item_install.html
+++ b/InvenTree/stock/templates/stock/item_install.html
@@ -3,15 +3,31 @@
{% block pre_form_content %}
+{% if install_item %}
- {% trans "Install another StockItem into this item." %}
+ {% trans "Install another Stock Item into this item." %}
{% trans "Stock items can only be installed if they meet the following criteria" %}:
- - {% trans "The StockItem links to a Part which is in the BOM for this StockItem" %}
- - {% trans "The StockItem is currently in stock" %}
+ - {% trans "The Stock Item links to a Part which is in the BOM for this Stock Item" %}
+ - {% trans "The Stock Item is currently in stock" %}
+ - {% trans "The Stock Item is serialized and does not belong to another item" %}
+{% elif install_in %}
+
+ {% trans "Install this Stock Item in another stock item." %}
+
+
+ {% trans "Stock items can only be installed if they meet the following criteria" %}:
+
+
+ - {% trans "The part associated to this Stock Item belongs to another part's BOM" %}
+ - {% trans "This Stock Item is serialized and does not belong to another item" %}
+
+
+{% endif %}
+
{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py
index 80968e5aa9..25f8cefac1 100644
--- a/InvenTree/stock/views.py
+++ b/InvenTree/stock/views.py
@@ -518,36 +518,73 @@ class StockItemInstall(AjaxUpdateView):
part = None
+ def get_params(self):
+ """ Retrieve GET parameters """
+
+ # Look at GET params
+ self.part_id = self.request.GET.get('part', None)
+ self.install_in = self.request.GET.get('install_in', False)
+ self.install_item = self.request.GET.get('install_item', False)
+
+ if self.part_id is None:
+ # Look at POST params
+ self.part_id = self.request.POST.get('part', None)
+
+ try:
+ self.part = Part.objects.get(pk=self.part_id)
+ except (ValueError, Part.DoesNotExist):
+ self.part = None
+
def get_stock_items(self):
"""
Return a list of stock items suitable for displaying to the user.
Requirements:
- Items must be in stock
-
- Filters:
- - Items can be filtered by Part reference
+ - Items must be in BOM of stock item
+ - Items must be serialized
"""
-
+
+ # Filter items in stock
items = StockItem.objects.filter(StockItem.IN_STOCK_FILTER)
- # Filter by Part association
+ # Filter serialized stock items
+ items = items.exclude(serial__isnull=True).exclude(serial__exact='')
- # Look at GET params
- part_id = self.request.GET.get('part', None)
+ if self.part:
+ # Filter for parts to install this item in
+ if self.install_in:
+ # Get parts using this part
+ allowed_parts = self.part.get_used_in()
+ # Filter
+ items = items.filter(part__in=allowed_parts)
- if part_id is None:
- # Look at POST params
- part_id = self.request.POST.get('part', None)
-
- try:
- self.part = Part.objects.get(pk=part_id)
- items = items.filter(part=self.part)
- except (ValueError, Part.DoesNotExist):
- self.part = None
+ # Filter for parts to install in this item
+ if self.install_item:
+ # Get parts used in this part's BOM
+ bom_items = self.part.get_bom_items()
+ allowed_parts = [item.sub_part for item in bom_items]
+ # Filter
+ items = items.filter(part__in=allowed_parts)
return items
+ def get_context_data(self, **kwargs):
+ """ Retrieve parameters and update context """
+
+ ctx = super().get_context_data(**kwargs)
+
+ # Get request parameters
+ self.get_params()
+
+ ctx.update({
+ 'part': self.part,
+ 'install_in': self.install_in,
+ 'install_item': self.install_item,
+ })
+
+ return ctx
+
def get_initial(self):
initials = super().get_initial()
@@ -558,11 +595,17 @@ class StockItemInstall(AjaxUpdateView):
if items.count() == 1:
item = items.first()
initials['stock_item'] = item.pk
- initials['quantity_to_install'] = item.quantity
+ # initials['quantity_to_install'] = item.quantity
if self.part:
initials['part'] = self.part
+ try:
+ # Is this stock item being installed in the other stock item?
+ initials['to_install'] = self.install_in or not self.install_item
+ except AttributeError:
+ pass
+
return initials
def get_form(self):
@@ -575,6 +618,8 @@ class StockItemInstall(AjaxUpdateView):
def post(self, request, *args, **kwargs):
+ self.get_params()
+
form = self.get_form()
valid = form.is_valid()
@@ -584,13 +629,20 @@ class StockItemInstall(AjaxUpdateView):
data = form.cleaned_data
other_stock_item = data['stock_item']
- quantity = data['quantity_to_install']
+ # quantity = data['quantity_to_install']
+ # Quantity will always be 1 for serialized item
+ quantity = 1
notes = data['notes']
- # Install the other stock item into this one
+ # Get stock item
this_stock_item = self.get_object()
- this_stock_item.installStockItem(other_stock_item, quantity, request.user, notes)
+ if data['to_install']:
+ # Install this stock item into the other stock item
+ other_stock_item.installStockItem(this_stock_item, quantity, request.user, notes)
+ else:
+ # Install the other stock item into this one
+ this_stock_item.installStockItem(other_stock_item, quantity, request.user, notes)
data = {
'form_valid': valid,
From 1c4924a4a5ebb95c52cc6a40c4c8959954d98544 Mon Sep 17 00:00:00 2001
From: eeintech
Date: Mon, 2 Aug 2021 15:14:55 -0400
Subject: [PATCH 02/28] Style duh
---
InvenTree/stock/forms.py | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py
index b088a6cdef..c0d6e9026f 100644
--- a/InvenTree/stock/forms.py
+++ b/InvenTree/stock/forms.py
@@ -8,7 +8,6 @@ from __future__ import unicode_literals
from django import forms
from django.forms.utils import ErrorDict
from django.utils.translation import ugettext_lazy as _
-from django.core.validators import MinValueValidator
from django.core.exceptions import ValidationError
from mptt.fields import TreeNodeChoiceField
@@ -242,8 +241,8 @@ class InstallStockForm(HelperForm):
)
to_install = forms.BooleanField(
- widget=forms.HiddenInput(),
- required=False
+ widget=forms.HiddenInput(),
+ required=False,
)
# quantity_to_install = RoundingDecimalFormField(
From 172a08fbba3b100c09ca5875ee12507efb0661d5 Mon Sep 17 00:00:00 2001
From: eeintech
Date: Tue, 3 Aug 2021 09:53:08 -0400
Subject: [PATCH 03/28] Removed old quantity setting lines
---
InvenTree/stock/forms.py | 10 ----------
InvenTree/stock/views.py | 2 --
2 files changed, 12 deletions(-)
diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py
index c0d6e9026f..7e739306b0 100644
--- a/InvenTree/stock/forms.py
+++ b/InvenTree/stock/forms.py
@@ -245,16 +245,6 @@ class InstallStockForm(HelperForm):
required=False,
)
- # quantity_to_install = RoundingDecimalFormField(
- # max_digits=10, decimal_places=5,
- # initial=1,
- # label=_('Quantity'),
- # help_text=_('Stock quantity to assign'),
- # validators=[
- # MinValueValidator(0.001)
- # ]
- # )
-
notes = forms.CharField(
required=False,
help_text=_('Notes')
diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py
index 25f8cefac1..8214ae75b4 100644
--- a/InvenTree/stock/views.py
+++ b/InvenTree/stock/views.py
@@ -595,7 +595,6 @@ class StockItemInstall(AjaxUpdateView):
if items.count() == 1:
item = items.first()
initials['stock_item'] = item.pk
- # initials['quantity_to_install'] = item.quantity
if self.part:
initials['part'] = self.part
@@ -629,7 +628,6 @@ class StockItemInstall(AjaxUpdateView):
data = form.cleaned_data
other_stock_item = data['stock_item']
- # quantity = data['quantity_to_install']
# Quantity will always be 1 for serialized item
quantity = 1
notes = data['notes']
From 29c8daed0af0b2a9c06106e03b7bb03bc0a9724b Mon Sep 17 00:00:00 2001
From: eeintech
Date: Tue, 3 Aug 2021 12:21:44 -0400
Subject: [PATCH 04/28] 'has_ipn' filter method did not return queryset
---
InvenTree/part/api.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py
index 3612a1c9f9..c0d049ecc7 100644
--- a/InvenTree/part/api.py
+++ b/InvenTree/part/api.py
@@ -443,6 +443,8 @@ class PartFilter(rest_filters.FilterSet):
else:
queryset = queryset.filter(IPN='')
+ return queryset
+
# Regex filter for name
name_regex = rest_filters.CharFilter(label='Filter by name (regex)', field_name='name', lookup_expr='iregex')
From fa3c5ae1081c1b709db50fe82b3ec4d7c3a5edc0 Mon Sep 17 00:00:00 2001
From: Matthias
Date: Wed, 4 Aug 2021 00:45:56 +0200
Subject: [PATCH 05/28] updating language to be clearer see
https://github.com/inventree/InvenTree/issues/1889#issuecomment-891901070
---
InvenTree/part/templates/part/detail.html | 6 +++---
InvenTree/part/templates/part/prices.html | 2 +-
InvenTree/templates/js/translated/bom.js | 4 ++--
3 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html
index 00d7f01e47..59aec17944 100644
--- a/InvenTree/part/templates/part/detail.html
+++ b/InvenTree/part/templates/part/detail.html
@@ -899,7 +899,7 @@
{% for line in price_history %}'{{ line.date }}',{% endfor %}
],
datasets: [{
- label: '{% blocktrans %}Single Price - {{currency}}{% endblocktrans %}',
+ label: '{% blocktrans %}Purchase Unit Price - {{currency}}{% endblocktrans %}',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
borderColor: 'rgb(255, 99, 132)',
yAxisID: 'y',
@@ -911,7 +911,7 @@
},
{% if 'price_diff' in price_history.0 %}
{
- label: '{% blocktrans %}Single Price Difference - {{currency}}{% endblocktrans %}',
+ label: '{% blocktrans %}Unit Price-Cost Difference - {{currency}}{% endblocktrans %}',
backgroundColor: 'rgba(68, 157, 68, 0.2)',
borderColor: 'rgb(68, 157, 68)',
yAxisID: 'y2',
@@ -923,7 +923,7 @@
hidden: true,
},
{
- label: '{% blocktrans %}Part Single Price - {{currency}}{% endblocktrans %}',
+ label: '{% blocktrans %}Supplier Unit Cost - {{currency}}{% endblocktrans %}',
backgroundColor: 'rgba(70, 127, 155, 0.2)',
borderColor: 'rgb(70, 127, 155)',
yAxisID: 'y',
diff --git a/InvenTree/part/templates/part/prices.html b/InvenTree/part/templates/part/prices.html
index 7581d659e1..e498bc09ba 100644
--- a/InvenTree/part/templates/part/prices.html
+++ b/InvenTree/part/templates/part/prices.html
@@ -161,7 +161,7 @@
{% trans 'Stock Pricing' %}
+ The Supplier Unit Cost is the current purchase price for that supplier part.">
{% if price_history|length > 0 %}
diff --git a/InvenTree/templates/js/translated/bom.js b/InvenTree/templates/js/translated/bom.js
index 32166d972a..20829bad79 100644
--- a/InvenTree/templates/js/translated/bom.js
+++ b/InvenTree/templates/js/translated/bom.js
@@ -262,13 +262,13 @@ function loadBomTable(table, options) {
cols.push(
{
field: 'price_range',
- title: '{% trans "Buy Price" %}',
+ title: '{% trans "Supplier Cost" %}',
sortable: true,
formatter: function(value, row, index, field) {
if (value) {
return value;
} else {
- return "
{% trans 'No pricing available' %}";
+ return "
{% trans 'No supplier pricing available' %}";
}
}
});
From 83d8226ad6ecba3e9f1aea3a8199ef4c92c07e55 Mon Sep 17 00:00:00 2001
From: Oliver
Date: Wed, 4 Aug 2021 11:33:20 +1000
Subject: [PATCH 06/28] Refactor "CreatePartCategory" form to API
(cherry picked from commit 06ff961564cdbece29ea52f4e681c079d98bcec8)
---
InvenTree/part/templates/part/category.html | 65 +++++----------------
InvenTree/part/test_views.py | 13 -----
InvenTree/part/urls.py | 3 -
InvenTree/part/views.py | 44 --------------
InvenTree/templates/js/translated/part.js | 31 ++++++++++
5 files changed, 46 insertions(+), 110 deletions(-)
diff --git a/InvenTree/part/templates/part/category.html b/InvenTree/part/templates/part/category.html
index 0aee97a5e3..1c41092574 100644
--- a/InvenTree/part/templates/part/category.html
+++ b/InvenTree/part/templates/part/category.html
@@ -240,32 +240,20 @@
});
$("#cat-create").click(function() {
- launchModalForm(
- "{% url 'category-create' %}",
- {
- follow: true,
- data: {
- {% if category %}
- category: {{ category.id }}
- {% endif %}
- },
- secondary: [
- {
- field: 'default_location',
- label: '{% trans "New Location" %}',
- title: '{% trans "Create new location" %}',
- url: "{% url 'stock-location-create' %}",
- },
- {
- field: 'parent',
- label: '{% trans "New Category" %}',
- title: '{% trans "Create new category" %}',
- url: "{% url 'category-create' %}",
- },
- ]
- }
- );
- })
+
+ var fields = categoryFields();
+
+ {% if category %}
+ fields.parent.value = {{ category.pk }};
+ {% endif %}
+
+ constructForm('{% url "api-part-category-list" %}', {
+ fields: fields,
+ method: 'POST',
+ title: '{% trans "Create Part Category" %}',
+ follow: true,
+ });
+ });
$("#part-export").click(function() {
@@ -286,12 +274,6 @@
{% endif %}
},
secondary: [
- {
- field: 'category',
- label: '{% trans "New Category" %}',
- title: '{% trans "Create new Part Category" %}',
- url: "{% url 'category-create' %}",
- },
{
field: 'default_location',
label: '{% trans "New Location" %}',
@@ -307,24 +289,7 @@
{% if category %}
$("#cat-edit").click(function () {
- constructForm(
- '{% url "api-part-category-detail" category.pk %}',
- {
- fields: {
- name: {},
- description: {},
- parent: {
- help_text: '{% trans "Select parent category" %}',
- },
- default_location: {},
- default_keywords: {
- icon: 'fa-key',
- }
- },
- title: '{% trans "Edit Part Category" %}',
- reload: true
- }
- );
+ editCategory({{ category.pk }});
});
{% if category.parent %}
diff --git a/InvenTree/part/test_views.py b/InvenTree/part/test_views.py
index 9779aac544..139ec20479 100644
--- a/InvenTree/part/test_views.py
+++ b/InvenTree/part/test_views.py
@@ -243,19 +243,6 @@ class PartQRTest(PartViewTestCase):
class CategoryTest(PartViewTestCase):
""" Tests for PartCategory related views """
- def test_create(self):
- """ Test view for creating a new category """
- response = self.client.get(reverse('category-create'), {'category': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
-
- self.assertEqual(response.status_code, 200)
-
- def test_create_invalid_parent(self):
- """ test creation of a new category with an invalid parent """
- response = self.client.get(reverse('category-create'), {'category': 9999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
-
- # Form should still return OK
- self.assertEqual(response.status_code, 200)
-
def test_set_category(self):
""" Test that the "SetCategory" view works """
diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py
index 2215e14785..62061f8279 100644
--- a/InvenTree/part/urls.py
+++ b/InvenTree/part/urls.py
@@ -65,9 +65,6 @@ category_parameter_urls = [
category_urls = [
- # Create a new category
- url(r'^new/', views.CategoryCreate.as_view(), name='category-create'),
-
# Top level subcategory display
url(r'^subcategory/', views.PartIndex.as_view(template_name='part/subcategory.html'), name='category-index-subcategory'),
diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py
index fb69241a10..dd79a5360b 100644
--- a/InvenTree/part/views.py
+++ b/InvenTree/part/views.py
@@ -12,7 +12,6 @@ from django.db.utils import IntegrityError
from django.shortcuts import get_object_or_404
from django.shortcuts import HttpResponseRedirect
from django.utils.translation import gettext_lazy as _
-from django.urls import reverse, reverse_lazy
from django.views.generic import DetailView, ListView
from django.forms.models import model_to_dict
from django.forms import HiddenInput, CheckboxInput
@@ -1905,49 +1904,6 @@ class CategoryDelete(AjaxDeleteView):
}
-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_template_name = 'modal_form.html'
- form_class = part_forms.EditCategoryForm
-
- def get_context_data(self, **kwargs):
- """ Add extra context data to template.
-
- - If parent category provided, pass the category details to the template
- """
- context = super(CategoryCreate, self).get_context_data(**kwargs).copy()
-
- parent_id = self.request.GET.get('category', None)
-
- if parent_id:
- try:
- context['category'] = PartCategory.objects.get(pk=parent_id)
- except PartCategory.DoesNotExist:
- pass
-
- return context
-
- def get_initial(self):
- """ Get initial data for new PartCategory
-
- - If parent provided, pre-fill the parent category
- """
- initials = super(CategoryCreate, self).get_initial().copy()
-
- parent_id = self.request.GET.get('category', None)
-
- if parent_id:
- try:
- initials['parent'] = PartCategory.objects.get(pk=parent_id)
- except PartCategory.DoesNotExist:
- pass
-
- return initials
-
-
class CategoryParameterTemplateCreate(AjaxCreateView):
""" View for creating a new PartCategoryParameterTemplate """
diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js
index 169c722d79..aaee9e47a0 100644
--- a/InvenTree/templates/js/translated/part.js
+++ b/InvenTree/templates/js/translated/part.js
@@ -14,6 +14,37 @@ function yesNoLabel(value) {
}
+function categoryFields() {
+ return {
+ parent: {
+ help_text: '{% trans "Parent part category" %}',
+ },
+ name: {},
+ description: {},
+ default_location: {},
+ default_keywords: {
+ icon: 'fa-key',
+ }
+ };
+}
+
+
+// Edit a PartCategory via the API
+function editCategory(pk, options={}) {
+
+ var url = `/api/part/category/${pk}/`;
+
+ var fields = categoryFields();
+
+ constructForm(url, {
+ fields: fields,
+ title: '{% trans "Edit Part Category" %}',
+ reload: true,
+ });
+
+}
+
+
function editPart(pk, options={}) {
var url = `/api/part/${pk}/`;
From 989983bdb5ead5e35b2a7f1e7b1fee11945d342f Mon Sep 17 00:00:00 2001
From: Oliver
Date: Wed, 4 Aug 2021 11:37:59 +1000
Subject: [PATCH 07/28] Fixed missing import
---
InvenTree/part/views.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py
index dd79a5360b..dd2868b72b 100644
--- a/InvenTree/part/views.py
+++ b/InvenTree/part/views.py
@@ -12,6 +12,7 @@ from django.db.utils import IntegrityError
from django.shortcuts import get_object_or_404
from django.shortcuts import HttpResponseRedirect
from django.utils.translation import gettext_lazy as _
+from django.urls import reverse
from django.views.generic import DetailView, ListView
from django.forms.models import model_to_dict
from django.forms import HiddenInput, CheckboxInput
From f95346f21458204530dc445a54d1c714ec168e63 Mon Sep 17 00:00:00 2001
From: Oliver
Date: Wed, 4 Aug 2021 12:10:49 +1000
Subject: [PATCH 08/28] Make the part thumbnail selection window searchable
---
InvenTree/part/api.py | 16 +++++++++++++---
InvenTree/part/templates/part/part_base.html | 10 +++++++---
InvenTree/templates/js/translated/tables.js | 2 +-
3 files changed, 21 insertions(+), 7 deletions(-)
diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py
index c0d049ecc7..773631459c 100644
--- a/InvenTree/part/api.py
+++ b/InvenTree/part/api.py
@@ -338,9 +338,7 @@ class PartThumbs(generics.ListAPIView):
- Images may be used for multiple parts!
"""
- queryset = self.get_queryset()
-
- # TODO - We should return the thumbnails here, not the full image!
+ queryset = self.filter_queryset(self.get_queryset())
# Return the most popular parts first
data = queryset.values(
@@ -349,6 +347,18 @@ class PartThumbs(generics.ListAPIView):
return Response(data)
+ filter_backends = [
+ filters.SearchFilter,
+ ]
+
+ search_fields = [
+ 'name',
+ 'description',
+ 'IPN',
+ 'revision',
+ 'keywords',
+ 'category__name',
+ ]
class PartThumbsUpdate(generics.RetrieveUpdateAPIView):
""" API endpoint for updating Part thumbnails"""
diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html
index 5191399f0a..ec637412a8 100644
--- a/InvenTree/part/templates/part/part_base.html
+++ b/InvenTree/part/templates/part/part_base.html
@@ -415,13 +415,16 @@
// 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,
+ $("#modal-form").find("#image-select-table").inventreeTable({
url: "{% url 'api-part-thumbs' %}",
showHeader: false,
+ showColumns: false,
clickToSelect: true,
+ sidePagination: 'server',
singleSelect: true,
+ formatNoMatches: function() {
+ return '{% trans "No matching images found" %}';
+ },
columns: [
{
checkbox: true,
@@ -429,6 +432,7 @@
{
field: 'image',
title: 'Image',
+ searchable: true,
formatter: function(value, row, index, field) {
return ""
}
diff --git a/InvenTree/templates/js/translated/tables.js b/InvenTree/templates/js/translated/tables.js
index afe1fefbc9..88d9a5f99a 100644
--- a/InvenTree/templates/js/translated/tables.js
+++ b/InvenTree/templates/js/translated/tables.js
@@ -187,7 +187,7 @@ $.fn.inventreeTable = function(options) {
if (!options.disablePagination) {
options.pagination = true;
options.paginationVAlign = options.paginationVAlign || 'both';
- options.pageSize = inventreeLoad(varName, 25);
+ options.pageSize = options.pageSize || inventreeLoad(varName, 25);
options.pageList = [25, 50, 100, 250, 'all'];
options.totalField = 'count';
options.dataField = 'results';
From 56c0e289bd7544c1a9b3a87b79b954dca5b30f67 Mon Sep 17 00:00:00 2001
From: Oliver
Date: Wed, 4 Aug 2021 12:13:24 +1000
Subject: [PATCH 09/28] Style fix
---
InvenTree/part/api.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py
index 773631459c..a01b05034f 100644
--- a/InvenTree/part/api.py
+++ b/InvenTree/part/api.py
@@ -360,6 +360,7 @@ class PartThumbs(generics.ListAPIView):
'category__name',
]
+
class PartThumbsUpdate(generics.RetrieveUpdateAPIView):
""" API endpoint for updating Part thumbnails"""
From 1f70538b043a03168e1666b01d8fe450a58025ee Mon Sep 17 00:00:00 2001
From: Oliver
Date: Wed, 4 Aug 2021 14:24:17 +1000
Subject: [PATCH 10/28] Adds a button to tables to reload data
---
InvenTree/templates/js/translated/filters.js | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/InvenTree/templates/js/translated/filters.js b/InvenTree/templates/js/translated/filters.js
index 4ee08affdf..bc0dc1b958 100644
--- a/InvenTree/templates/js/translated/filters.js
+++ b/InvenTree/templates/js/translated/filters.js
@@ -265,6 +265,8 @@ function setupFilterList(tableKey, table, target) {
// One blank slate, please
element.empty();
+ element.append(``);
+
element.append(``);
if (Object.keys(filters).length > 0) {
@@ -279,6 +281,11 @@ function setupFilterList(tableKey, table, target) {
element.append(`${title} = ${value}x
`);
}
+ // Callback for reloading the table
+ element.find(`#reload-${tableKey}`).click(function() {
+ $(table).bootstrapTable('refresh');
+ });
+
// Add a callback for adding a new filter
element.find(`#${add}`).click(function clicked() {
From 75a1be0284244e79ba08d986c2957d9e852d4955 Mon Sep 17 00:00:00 2001
From: Oliver Walters
Date: Wed, 4 Aug 2021 17:25:51 +1000
Subject: [PATCH 11/28] Use API forms for creating and editing BomItem objects
---
InvenTree/part/forms.py | 28 -----
InvenTree/part/templates/part/detail.html | 30 ++---
InvenTree/part/urls.py | 10 --
InvenTree/part/views.py | 128 ----------------------
InvenTree/templates/js/translated/bom.js | 35 ++++--
5 files changed, 43 insertions(+), 188 deletions(-)
diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py
index 9523550198..1fc2848440 100644
--- a/InvenTree/part/forms.py
+++ b/InvenTree/part/forms.py
@@ -18,7 +18,6 @@ import common.models
from common.forms import MatchItemForm
from .models import Part, PartCategory, PartRelated
-from .models import BomItem
from .models import PartParameterTemplate, PartParameter
from .models import PartCategoryParameterTemplate
from .models import PartSellPriceBreak, PartInternalPriceBreak
@@ -317,33 +316,6 @@ class EditCategoryParameterTemplateForm(HelperForm):
]
-class EditBomItemForm(HelperForm):
- """ Form for editing a BomItem object """
-
- quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5, label=_('Quantity'))
-
- sub_part = PartModelChoiceField(queryset=Part.objects.all(), label=_('Sub part'))
-
- class Meta:
- model = BomItem
- fields = [
- 'part',
- 'sub_part',
- 'quantity',
- 'reference',
- 'overage',
- 'note',
- 'allow_variants',
- 'inherited',
- 'optional',
- ]
-
- # Prevent editing of the part associated with this BomItem
- widgets = {
- 'part': forms.HiddenInput()
- }
-
-
class PartPriceForm(forms.Form):
""" Simple form for viewing part pricing information """
diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html
index 59aec17944..267b880d49 100644
--- a/InvenTree/part/templates/part/detail.html
+++ b/InvenTree/part/templates/part/detail.html
@@ -440,22 +440,22 @@
});
$("#bom-item-new").click(function () {
- launchModalForm(
- "{% url 'bom-item-create' %}?parent={{ part.id }}",
- {
- success: function() {
- $("#bom-table").bootstrapTable('refresh');
- },
- secondary: [
- {
- field: 'sub_part',
- label: '{% trans "New Part" %}',
- title: '{% trans "Create New Part" %}',
- url: "{% url 'part-create' %}",
- },
- ]
+
+ var fields = bomItemFields();
+
+ fields.part.value = {{ part.pk }};
+ fields.sub_part.filters = {
+ active: true,
+ };
+
+ constructForm('{% url "api-bom-list" %}', {
+ fields: fields,
+ method: 'POST',
+ title: '{% trans "Create BOM Item" %}',
+ onSuccess: function() {
+ $('#bom-table').bootstrapTable('refresh');
}
- );
+ });
});
{% else %}
diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py
index 62061f8279..52e9b929c1 100644
--- a/InvenTree/part/urls.py
+++ b/InvenTree/part/urls.py
@@ -78,10 +78,6 @@ category_urls = [
]))
]
-part_bom_urls = [
- url(r'^edit/?', views.BomItemEdit.as_view(), name='bom-item-edit'),
-]
-
# URL list for part web interface
part_urls = [
@@ -92,9 +88,6 @@ part_urls = [
url(r'^import/', views.PartImport.as_view(), name='part-import'),
url(r'^import-api/', views.PartImportAjax.as_view(), name='api-part-import'),
- # Create a new BOM item
- url(r'^bom/new/?', views.BomItemCreate.as_view(), name='bom-item-create'),
-
# Download a BOM upload template
url(r'^bom_template/?', views.BomUploadTemplate.as_view(), name='bom-upload-template'),
@@ -122,9 +115,6 @@ part_urls = [
# Change category for multiple parts
url(r'^set-category/?', views.PartSetCategory.as_view(), name='part-set-category'),
- # Bom Items
- url(r'^bom/(?P\d+)/', include(part_bom_urls)),
-
# Individual part using IPN as slug
url(r'^(?P[-\w]+)/', views.PartDetailFromIPN.as_view(), name='part-detail-from-ipn'),
diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py
index dd2868b72b..b35e752351 100644
--- a/InvenTree/part/views.py
+++ b/InvenTree/part/views.py
@@ -2078,134 +2078,6 @@ class CategoryParameterTemplateDelete(AjaxDeleteView):
return self.object
-class BomItemCreate(AjaxCreateView):
- """
- Create view for making a new BomItem object
- """
-
- model = BomItem
- form_class = part_forms.EditBomItemForm
- ajax_template_name = 'modal_form.html'
- ajax_form_title = _('Create BOM Item')
-
- def get_form(self):
- """ Override get_form() method to reduce Part selection options.
-
- - Do not allow part to be added to its own BOM
- - Remove any Part items that are already in the BOM
- """
-
- form = super(AjaxCreateView, self).get_form()
-
- part_id = form['part'].value()
-
- # Construct a queryset for the part field
- part_query = Part.objects.filter(active=True)
-
- # Construct a queryset for the sub_part field
- sub_part_query = Part.objects.filter(
- component=True,
- active=True
- )
-
- try:
- part = Part.objects.get(id=part_id)
-
- # Hide the 'part' field
- form.fields['part'].widget = HiddenInput()
-
- # Exclude the part from its own BOM
- sub_part_query = sub_part_query.exclude(id=part.id)
-
- # Eliminate any options that are already in the BOM!
- sub_part_query = sub_part_query.exclude(id__in=[item.id for item in part.getRequiredParts()])
-
- except (ValueError, Part.DoesNotExist):
- pass
-
- # Set the querysets for the fields
- form.fields['part'].queryset = part_query
- form.fields['sub_part'].queryset = sub_part_query
-
- return form
-
- def get_initial(self):
- """ Provide initial data for the BomItem:
-
- - If 'parent' provided, set the parent part field
- """
-
- # Look for initial values
- initials = super(BomItemCreate, self).get_initial().copy()
-
- # Parent part for this item?
- parent_id = self.request.GET.get('parent', None)
-
- if parent_id:
- try:
- initials['part'] = Part.objects.get(pk=parent_id)
- except Part.DoesNotExist:
- pass
-
- return initials
-
-
-class BomItemEdit(AjaxUpdateView):
- """ Update view for editing BomItem """
-
- model = BomItem
- form_class = part_forms.EditBomItemForm
- ajax_template_name = 'modal_form.html'
- ajax_form_title = _('Edit BOM item')
-
- def get_form(self):
- """ Override get_form() method to filter part selection options
-
- - Do not allow part to be added to its own BOM
- - Remove any part items that are already in the BOM
- """
-
- item = self.get_object()
-
- form = super().get_form()
-
- part_id = form['part'].value()
-
- try:
- part = Part.objects.get(pk=part_id)
-
- # Construct a queryset
- query = Part.objects.filter(component=True)
-
- # Limit to "active" items, *unless* the currently selected item is not active
- if item.sub_part.active:
- query = query.filter(active=True)
-
- # Prevent the parent part from being selected
- query = query.exclude(pk=part_id)
-
- # Eliminate any options that are already in the BOM,
- # *except* for the item which is already selected
- try:
- sub_part_id = int(form['sub_part'].value())
- except ValueError:
- sub_part_id = -1
-
- existing = [item.pk for item in part.getRequiredParts()]
-
- if sub_part_id in existing:
- existing.remove(sub_part_id)
-
- query = query.exclude(id__in=existing)
-
- form.fields['sub_part'].queryset = query
-
- except (ValueError, Part.DoesNotExist):
- pass
-
- return form
-
-
class PartSalePriceBreakCreate(AjaxCreateView):
"""
View for creating a sale price break for a part
diff --git a/InvenTree/templates/js/translated/bom.js b/InvenTree/templates/js/translated/bom.js
index 20829bad79..34a6206ac9 100644
--- a/InvenTree/templates/js/translated/bom.js
+++ b/InvenTree/templates/js/translated/bom.js
@@ -8,6 +8,26 @@
*/
+function bomItemFields() {
+
+ return {
+ part: {
+ hidden: true,
+ },
+ sub_part: {
+ },
+ quantity: {},
+ reference: {},
+ overage: {},
+ note: {},
+ allow_variants: {},
+ inherited: {},
+ optional: {},
+ };
+
+}
+
+
function reloadBomTable(table, options) {
table.bootstrapTable('refresh');
@@ -528,14 +548,15 @@ function loadBomTable(table, options) {
var pk = $(this).attr('pk');
var url = `/part/bom/${pk}/edit/`;
- launchModalForm(
- url,
- {
- success: function() {
- reloadBomTable(table);
- }
+ var fields = bomItemFields();
+
+ constructForm(`/api/bom/${pk}/`, {
+ fields: fields,
+ title: '{% trans "Edit BOM Item" %}',
+ onSuccess: function() {
+ reloadBomTable(table);
}
- );
+ });
});
table.on('click', '.bom-validate-button', function() {
From 2e8a490ca9cbb18b67309da5c15b8d42103dbbb0 Mon Sep 17 00:00:00 2001
From: Oliver Walters
Date: Wed, 4 Aug 2021 17:41:47 +1000
Subject: [PATCH 12/28] Fixes for unit tests
---
InvenTree/part/test_views.py | 19 -------------------
1 file changed, 19 deletions(-)
diff --git a/InvenTree/part/test_views.py b/InvenTree/part/test_views.py
index 139ec20479..206d4dd56a 100644
--- a/InvenTree/part/test_views.py
+++ b/InvenTree/part/test_views.py
@@ -259,22 +259,3 @@ class CategoryTest(PartViewTestCase):
response = self.client.post(url, data, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
-
-
-class BomItemTests(PartViewTestCase):
- """ Tests for BomItem related views """
-
- def test_create_valid_parent(self):
- """ Create a BomItem for a valid part """
- response = self.client.get(reverse('bom-item-create'), {'parent': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
- self.assertEqual(response.status_code, 200)
-
- def test_create_no_parent(self):
- """ Create a BomItem without a parent """
- response = self.client.get(reverse('bom-item-create'), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
- self.assertEqual(response.status_code, 200)
-
- def test_create_invalid_parent(self):
- """ Create a BomItem with an invalid parent """
- response = self.client.get(reverse('bom-item-create'), {'parent': 99999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
- self.assertEqual(response.status_code, 200)
From a64ee23afc2aaf01ab5578dd571248ee5b6538b2 Mon Sep 17 00:00:00 2001
From: Oliver Walters
Date: Wed, 4 Aug 2021 23:16:11 +1000
Subject: [PATCH 13/28] Add more options for form rendering
- "before" a field
- "after" a field
- pure "eye candy" field
---
InvenTree/templates/js/translated/forms.js | 45 ++++++++++++++++++++--
1 file changed, 42 insertions(+), 3 deletions(-)
diff --git a/InvenTree/templates/js/translated/forms.js b/InvenTree/templates/js/translated/forms.js
index 4801ec77eb..46b2b21a87 100644
--- a/InvenTree/templates/js/translated/forms.js
+++ b/InvenTree/templates/js/translated/forms.js
@@ -366,6 +366,10 @@ function constructFormBody(fields, options) {
// TODO: Refactor the following code with Object.assign (see above)
+ // "before" and "after" renders
+ fields[field].before = field_options.before;
+ fields[field].after = field_options.after;
+
// Secondary modal options
fields[field].secondary = field_options.secondary;
@@ -560,10 +564,15 @@ function submitFormData(fields, options) {
var has_files = false;
// Extract values for each field
- options.field_names.forEach(function(name) {
+ for (var idx = 0; idx < options.fields_names.length; idx++) {
+
+ var name = options.field_names[idx];
var field = fields[name] || null;
+ // Ignore visual fields
+ if (field && field.type == 'candy') continue;
+
if (field) {
var value = getFormFieldValue(name, field, options);
@@ -593,7 +602,7 @@ function submitFormData(fields, options) {
} else {
console.log(`WARNING: Could not find field matching '${name}'`);
}
- });
+ }
var upload_func = inventreePut;
@@ -1279,6 +1288,11 @@ function renderModelData(name, model, data, parameters, options) {
*/
function constructField(name, parameters, options) {
+ // Shortcut for simple visual fields
+ if (parameters.type == 'candy') {
+ return constructCandyInput(name, parameters, options);
+ }
+
var field_name = `id_${name}`;
// Hidden inputs are rendered without label / help text / etc
@@ -1292,7 +1306,14 @@ function constructField(name, parameters, options) {
form_classes += ' has-error';
}
- var html = ``; // form-group
+ if (parameters.after) {
+ html += parameters.after;
+ }
+
return html;
}
@@ -1430,6 +1455,9 @@ function constructInput(name, parameters, options) {
case 'date':
func = constructDateInput;
break;
+ case 'candy':
+ func = constructCandyInput;
+ break;
default:
// Unsupported field type!
break;
@@ -1658,6 +1686,17 @@ function constructDateInput(name, parameters, options) {
}
+/*
+ * Construct a "candy" field input
+ * No actual field data!
+ */
+function constructCandyInput(name, parameters, options) {
+
+ return parameters.html;
+
+}
+
+
/*
* Construct a 'help text' div based on the field parameters
*
From 2bf3e3ab020a9030dd73d51c11ac426437d53a60 Mon Sep 17 00:00:00 2001
From: Oliver Walters
Date: Wed, 4 Aug 2021 23:26:17 +1000
Subject: [PATCH 14/28] Function to construct part form fields
---
InvenTree/part/api.py | 2 +
InvenTree/templates/js/translated/forms.js | 2 +-
InvenTree/templates/js/translated/part.js | 110 +++++++++++++++++++++
3 files changed, 113 insertions(+), 1 deletion(-)
diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py
index a01b05034f..3b91d27c81 100644
--- a/InvenTree/part/api.py
+++ b/InvenTree/part/api.py
@@ -23,6 +23,7 @@ from djmoney.money import Money
from djmoney.contrib.exchange.models import convert_money
from djmoney.contrib.exchange.exceptions import MissingRate
+from decimal import Decimal
from .models import Part, PartCategory, BomItem
from .models import PartParameter, PartParameterTemplate
@@ -30,6 +31,7 @@ from .models import PartAttachment, PartTestTemplate
from .models import PartSellPriceBreak, PartInternalPriceBreak
from .models import PartCategoryParameterTemplate
+from stock.models import StockItem
from common.models import InvenTreeSetting
from build.models import Build
diff --git a/InvenTree/templates/js/translated/forms.js b/InvenTree/templates/js/translated/forms.js
index 46b2b21a87..27337d97e7 100644
--- a/InvenTree/templates/js/translated/forms.js
+++ b/InvenTree/templates/js/translated/forms.js
@@ -564,7 +564,7 @@ function submitFormData(fields, options) {
var has_files = false;
// Extract values for each field
- for (var idx = 0; idx < options.fields_names.length; idx++) {
+ for (var idx = 0; idx < options.field_names.length; idx++) {
var name = options.field_names[idx];
diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js
index aaee9e47a0..fafdaa94e7 100644
--- a/InvenTree/templates/js/translated/part.js
+++ b/InvenTree/templates/js/translated/part.js
@@ -13,6 +13,116 @@ function yesNoLabel(value) {
}
}
+// Construct fieldset for part forms
+function partFields(options={}) {
+
+ var fields = {
+ category: {},
+ name: {},
+ IPN: {},
+ revision: {},
+ description: {},
+ variant_of: {},
+ keywords: {
+ icon: 'fa-key',
+ },
+ units: {},
+ link: {
+ icon: 'fa-link',
+ },
+ default_location: {},
+ default_supplier: {},
+ default_expiry: {
+ icon: 'fa-calendar-alt',
+ },
+ minimum_stock: {
+ icon: 'fa-boxes',
+ },
+ attributes: {
+ type: 'candy',
+ html: `
{% trans "Part Attributes" %}
`
+ },
+ component: {
+ value: global_settings.PART_COMPONENT,
+ },
+ assembly: {
+ value: global_settings.PART_ASSEMBLY,
+ },
+ is_template: {
+ value: global_settings.PART_TEMPLATE,
+ },
+ trackable: {
+ value: global_settings.PART_TRACKABLE,
+ },
+ purchaseable: {
+ value: global_settings.PART_PURCHASEABLE,
+ },
+ salable: {
+ value: global_settings.PART_SALABLE,
+ },
+ virtual: {
+ value: global_settings.PART_VIRTUAL,
+ },
+ };
+
+ // Pop expiry field
+ if (!global_settings.STOCK_ENABLE_EXPIRY) {
+ delete fields["default_expiry"];
+ }
+
+ // Additional fields when "creating" a new part
+ if (options.create) {
+
+ // No supplier parts available yet
+ delete fields["default_supplier"];
+
+ fields.create = {
+ type: 'candy',
+ html: `
{% trans "Part Creation Options" %}
`,
+ };
+
+ if (global_settings.PART_CREATE_INITIAL) {
+ fields.initial_stock = {
+ type: 'decimal',
+ label: '{% trans "Initial Stock Quantity" %}',
+ help_text: '{% trans "Initialize part stock with specified quantity" %}',
+ };
+ }
+
+ fields.copy_category_parameters = {
+ type: 'boolean',
+ label: '{% trans "Copy Category Parameters" %}',
+ help_text: '{% trans "Copy parameter templates from selected part category" %}',
+ value: global_settings.PART_CATEGORY_PARAMETERS,
+ };
+ }
+
+ // Additional fields when "duplicating" a part
+ if (options.duplicate) {
+
+ fields.duplicate = {
+ type: 'candy',
+ html: `
{% trans "Part Duplication Options" %}
`,
+ };
+
+ fields.copy_bom = {
+ type: 'boolean',
+ label: '{% trans "Copy BOM" %}',
+ help_text: '{% trans "Copy bill of materials from original part" %}',
+ value: global_settings.PART_COPY_BOM,
+ };
+
+ fields.copy_parameters = {
+ type: 'boolean',
+ label: '{% trans "Copy Parameters" %}',
+ help_text: '{% trans "Copy parameter data from original part" %}',
+ value: global_settings.PART_COPY_PARAMETERS,
+ };
+ }
+
+ return fields;
+}
+
function categoryFields() {
return {
From b04f22fc53848981c1a581f73ae21312eb9a43a6 Mon Sep 17 00:00:00 2001
From: Oliver Walters
Date: Wed, 4 Aug 2021 23:27:16 +1000
Subject: [PATCH 15/28] CreatePart form now uses the API
- Simplify the way category parameter templates are copied
---
InvenTree/common/models.py | 3 +-
InvenTree/part/api.py | 35 ++++-
InvenTree/part/models.py | 48 +++---
InvenTree/part/templates/part/category.html | 38 ++---
InvenTree/part/test_views.py | 13 --
InvenTree/part/urls.py | 3 -
InvenTree/part/views.py | 161 +-------------------
7 files changed, 74 insertions(+), 227 deletions(-)
diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py
index 5d75a4dd74..839780d5b4 100644
--- a/InvenTree/common/models.py
+++ b/InvenTree/common/models.py
@@ -637,7 +637,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'PART_PURCHASEABLE': {
'name': _('Purchaseable'),
'description': _('Parts are purchaseable by default'),
- 'default': False,
+ 'default': True,
'validator': bool,
},
@@ -662,6 +662,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'validator': bool,
},
+ # TODO: Remove this setting in future, new API forms make this not useful
'PART_SHOW_QUANTITY_IN_FORMS': {
'name': _('Show Quantity in Forms'),
'description': _('Display available part quantity in some forms'),
diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py
index 3b91d27c81..88866ad58c 100644
--- a/InvenTree/part/api.py
+++ b/InvenTree/part/api.py
@@ -630,16 +630,47 @@ class PartList(generics.ListCreateAPIView):
else:
return Response(data)
- def perform_create(self, serializer):
+ def create(self, request, *args, **kwargs):
"""
We wish to save the user who created this part!
Note: Implementation copied from DRF class CreateModelMixin
"""
+ serializer = self.get_serializer(data=request.data)
+ serializer.is_valid(raise_exception=True)
+
part = serializer.save()
part.creation_user = self.request.user
- part.save()
+
+ # Optionally copy templates from category or parent category
+ copy_templates = {
+ 'main': str2bool(request.data.get('copy_category_templates', False)),
+ 'parent': str2bool(request.data.get('copy_parent_templates', False))
+ }
+
+ part.save(**{'add_category_templates': copy_templates})
+
+ # Optionally create initial stock item
+ try:
+ initial_stock = Decimal(request.data.get('initial_stock', 0))
+
+ if initial_stock > 0 and part.default_location is not None:
+
+ stock_item = StockItem(
+ part=part,
+ quantity=initial_stock,
+ location=part.default_location,
+ )
+
+ stock_item.save(user=request.user)
+
+ except:
+ pass
+
+ headers = self.get_success_headers(serializer.data)
+
+ return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def get_queryset(self, *args, **kwargs):
diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py
index 2dd5d3ad7f..b75edde9cc 100644
--- a/InvenTree/part/models.py
+++ b/InvenTree/part/models.py
@@ -409,7 +409,7 @@ class Part(MPTTModel):
"""
# Get category templates settings
- add_category_templates = kwargs.pop('add_category_templates', None)
+ add_category_templates = kwargs.pop('add_category_templates', False)
if self.pk:
previous = Part.objects.get(pk=self.pk)
@@ -437,39 +437,29 @@ class Part(MPTTModel):
# Get part category
category = self.category
- if category and add_category_templates:
- # Store templates added to part
+ if category is not None:
+
template_list = []
- # Create part parameters for selected category
- category_templates = add_category_templates['main']
- if category_templates:
+ parent_categories = category.get_ancestors(include_self=True)
+
+ for category in parent_categories:
for template in category.get_parameter_templates():
- parameter = PartParameter.create(part=self,
- template=template.parameter_template,
- data=template.default_value,
- save=True)
- if parameter:
+ # Check that template wasn't already added
+ if template.parameter_template not in template_list:
+
template_list.append(template.parameter_template)
- # Create part parameters for parent category
- category_templates = add_category_templates['parent']
- if category_templates:
- # Get parent categories
- parent_categories = category.get_ancestors()
-
- for category in parent_categories:
- for template in category.get_parameter_templates():
- # Check that template wasn't already added
- if template.parameter_template not in template_list:
- try:
- PartParameter.create(part=self,
- template=template.parameter_template,
- data=template.default_value,
- save=True)
- except IntegrityError:
- # PartParameter already exists
- pass
+ try:
+ PartParameter.create(
+ part=self,
+ template=template.parameter_template,
+ data=template.default_value,
+ save=True
+ )
+ except IntegrityError:
+ # PartParameter already exists
+ pass
def __str__(self):
return f"{self.full_name} - {self.description}"
diff --git a/InvenTree/part/templates/part/category.html b/InvenTree/part/templates/part/category.html
index 1c41092574..b149fd28ed 100644
--- a/InvenTree/part/templates/part/category.html
+++ b/InvenTree/part/templates/part/category.html
@@ -264,25 +264,25 @@
{% if roles.part.add %}
$("#part-create").click(function() {
- launchModalForm(
- "{% url 'part-create' %}",
- {
- follow: true,
- data: {
- {% if category %}
- category: {{ category.id }}
- {% endif %}
- },
- secondary: [
- {
- field: 'default_location',
- label: '{% trans "New Location" %}',
- title: '{% trans "Create new Stock Location" %}',
- url: "{% url 'stock-location-create' %}",
- }
- ]
- }
- );
+
+ var fields = partFields({
+ create: true,
+ });
+
+ {% if category %}
+ fields.category.value = {{ category.pk }};
+ {% endif %}
+
+ constructForm('{% url "api-part-list" %}', {
+ method: 'POST',
+ fields: fields,
+ title: '{% trans "Create Part" %}',
+ onSuccess: function(data) {
+ // Follow the new part
+ location.href = `/part/${data.pk}/`;
+ },
+ });
+
});
{% endif %}
diff --git a/InvenTree/part/test_views.py b/InvenTree/part/test_views.py
index 206d4dd56a..c555687183 100644
--- a/InvenTree/part/test_views.py
+++ b/InvenTree/part/test_views.py
@@ -158,19 +158,6 @@ class PartDetailTest(PartViewTestCase):
class PartTests(PartViewTestCase):
""" Tests for Part forms """
- def test_part_create(self):
- """ Launch form to create a new part """
- response = self.client.get(reverse('part-create'), {'category': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
- self.assertEqual(response.status_code, 200)
-
- # And again, with an invalid category
- response = self.client.get(reverse('part-create'), {'category': 9999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
- self.assertEqual(response.status_code, 200)
-
- # And again, with no category
- response = self.client.get(reverse('part-create'), {'name': 'Test part'}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
- self.assertEqual(response.status_code, 200)
-
def test_part_duplicate(self):
""" Launch form to duplicate part """
diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py
index 52e9b929c1..0802a94f1a 100644
--- a/InvenTree/part/urls.py
+++ b/InvenTree/part/urls.py
@@ -81,9 +81,6 @@ category_urls = [
# URL list for part web interface
part_urls = [
- # Create a new part
- url(r'^new/?', views.PartCreate.as_view(), name='part-create'),
-
# Upload a part
url(r'^import/', views.PartImport.as_view(), name='part-import'),
url(r'^import-api/', views.PartImportAjax.as_view(), name='api-part-import'),
diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py
index b35e752351..3e4b6c59d7 100644
--- a/InvenTree/part/views.py
+++ b/InvenTree/part/views.py
@@ -44,7 +44,7 @@ from common.files import FileManager
from common.views import FileManagementFormView, FileManagementAjaxView
from common.forms import UploadFileForm, MatchFieldForm
-from stock.models import StockItem, StockLocation
+from stock.models import StockLocation
import common.settings as inventree_settings
@@ -438,165 +438,6 @@ class PartDuplicate(AjaxCreateView):
return initials
-class PartCreate(AjaxCreateView):
- """ View for creating a new Part object.
-
- Options for providing initial conditions:
-
- - Provide a category object as initial data
- """
- model = Part
- form_class = part_forms.EditPartForm
-
- ajax_form_title = _('Create New Part')
- ajax_template_name = 'part/create_part.html'
-
- def get_data(self):
- return {
- 'success': _("Created new part"),
- }
-
- def get_category_id(self):
- return self.request.GET.get('category', None)
-
- def get_context_data(self, **kwargs):
- """ Provide extra context information for the form to display:
-
- - Add category information (if provided)
- """
- context = super(PartCreate, self).get_context_data(**kwargs)
-
- # Add category information to the page
- cat_id = self.get_category_id()
-
- if cat_id:
- try:
- context['category'] = PartCategory.objects.get(pk=cat_id)
- except (PartCategory.DoesNotExist, ValueError):
- pass
-
- return context
-
- def get_form(self):
- """ Create Form for making new Part object.
- Remove the 'default_supplier' field as there are not yet any matching SupplierPart objects
- """
- form = super(AjaxCreateView, self).get_form()
-
- # Hide the "default expiry" field if the feature is not enabled
- if not inventree_settings.stock_expiry_enabled():
- form.fields['default_expiry'].widget = HiddenInput()
-
- # Hide the "initial stock amount" field if the feature is not enabled
- if not InvenTreeSetting.get_setting('PART_CREATE_INITIAL'):
- form.fields['initial_stock'].widget = HiddenInput()
-
- # Hide the default_supplier field (there are no matching supplier parts yet!)
- form.fields['default_supplier'].widget = HiddenInput()
-
- # Display category templates widgets
- form.fields['selected_category_templates'].widget = CheckboxInput()
- form.fields['parent_category_templates'].widget = CheckboxInput()
-
- return form
-
- def post(self, request, *args, **kwargs):
-
- form = self.get_form()
-
- context = {}
-
- valid = form.is_valid()
-
- name = request.POST.get('name', None)
-
- if name:
- matches = match_part_names(name)
-
- if len(matches) > 0:
-
- # Limit to the top 5 matches (to prevent clutter)
- context['matches'] = matches[:5]
-
- # Enforce display of the checkbox
- form.fields['confirm_creation'].widget = CheckboxInput()
-
- # Check if the user has checked the 'confirm_creation' input
- confirmed = str2bool(request.POST.get('confirm_creation', False))
-
- if not confirmed:
- msg = _('Possible matches exist - confirm creation of new part')
- form.add_error('confirm_creation', msg)
-
- form.pre_form_warning = msg
- valid = False
-
- data = {
- 'form_valid': valid
- }
-
- if valid:
- # Create the new Part
- part = form.save(commit=False)
-
- # Record the user who created this part
- part.creation_user = request.user
-
- # Store category templates settings
- add_category_templates = {
- 'main': form.cleaned_data['selected_category_templates'],
- 'parent': form.cleaned_data['parent_category_templates'],
- }
-
- # Save part and pass category template settings
- part.save(**{'add_category_templates': add_category_templates})
-
- # Add stock if set
- init_stock = int(request.POST.get('initial_stock', 0))
- if init_stock:
- stock = StockItem(part=part,
- quantity=init_stock,
- location=part.default_location)
- stock.save()
-
- data['pk'] = part.pk
- data['text'] = str(part)
-
- try:
- data['url'] = part.get_absolute_url()
- except AttributeError:
- pass
-
- return self.renderJsonResponse(request, form, data, context=context)
-
- def get_initial(self):
- """ Get initial data for the new Part object:
-
- - If a category is provided, pre-fill the Category field
- """
-
- initials = super(PartCreate, self).get_initial()
-
- if self.get_category_id():
- try:
- category = PartCategory.objects.get(pk=self.get_category_id())
- initials['category'] = category
- initials['keywords'] = category.default_keywords
- except (PartCategory.DoesNotExist, ValueError):
- pass
-
- # Allow initial data to be passed through as arguments
- for label in ['name', 'IPN', 'description', 'revision', 'keywords']:
- if label in self.request.GET:
- initials[label] = self.request.GET.get(label)
-
- # Automatically create part parameters from category templates
- initials['selected_category_templates'] = str2bool(InvenTreeSetting.get_setting('PART_CATEGORY_PARAMETERS', False))
- initials['parent_category_templates'] = initials['selected_category_templates']
-
- return initials
-
-
class PartImport(FileManagementFormView):
''' Part: Upload file, match to fields and import parts(using multi-Step form) '''
permission_required = 'part.add'
From 1fafaf857720ef578392e0444b963b1c5abd1208 Mon Sep 17 00:00:00 2001
From: Oliver Walters
Date: Wed, 4 Aug 2021 23:29:39 +1000
Subject: [PATCH 16/28] Refactor partfields function (was essentially
duplicated)
---
InvenTree/templates/js/translated/part.js | 81 +++--------------------
1 file changed, 8 insertions(+), 73 deletions(-)
diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js
index fafdaa94e7..988481d77c 100644
--- a/InvenTree/templates/js/translated/part.js
+++ b/InvenTree/templates/js/translated/part.js
@@ -65,6 +65,11 @@ function partFields(options={}) {
},
};
+ // If editing a part, we can set the "active" status
+ if (options.edit) {
+ fields.active = {};
+ }
+
// Pop expiry field
if (!global_settings.STOCK_ENABLE_EXPIRY) {
delete fields["default_expiry"];
@@ -159,79 +164,9 @@ function editPart(pk, options={}) {
var url = `/api/part/${pk}/`;
- var fields = {
- category: {
- /*
- secondary: {
- label: '{% trans "New Category" %}',
- title: '{% trans "Create New Part Category" %}',
- api_url: '{% url "api-part-category-list" %}',
- method: 'POST',
- fields: {
- name: {},
- description: {},
- parent: {
- secondary: {
- title: '{% trans "New Parent" %}',
- api_url: '{% url "api-part-category-list" %}',
- method: 'POST',
- fields: {
- name: {},
- description: {},
- parent: {},
- }
- }
- },
- }
- },
- */
- },
- name: {
- placeholder: 'part name',
- },
- IPN: {},
- description: {},
- revision: {},
- keywords: {
- icon: 'fa-key',
- },
- variant_of: {},
- link: {
- icon: 'fa-link',
- },
- default_location: {
- /*
- secondary: {
- label: '{% trans "New Location" %}',
- title: '{% trans "Create new stock location" %}',
- },
- */
- },
- default_supplier: {
- filters: {
- part: pk,
- part_detail: true,
- manufacturer_detail: true,
- supplier_detail: true,
- },
- /*
- secondary: {
- label: '{% trans "New Supplier Part" %}',
- title: '{% trans "Create new supplier part" %}',
- }
- */
- },
- units: {},
- minimum_stock: {},
- virtual: {},
- is_template: {},
- assembly: {},
- component: {},
- trackable: {},
- purchaseable: {},
- salable: {},
- active: {},
- };
+ var fields = partFields({
+ edit: true
+ });
constructForm(url, {
fields: fields,
From 408ff639ddb18e6c0d539aed7754f759dcea1bb3 Mon Sep 17 00:00:00 2001
From: Oliver Walters
Date: Wed, 4 Aug 2021 23:48:21 +1000
Subject: [PATCH 17/28] Adds ability to pre-fill a form with a complete dataset
---
InvenTree/templates/js/translated/forms.js | 17 ++++++++++++++++-
1 file changed, 16 insertions(+), 1 deletion(-)
diff --git a/InvenTree/templates/js/translated/forms.js b/InvenTree/templates/js/translated/forms.js
index 27337d97e7..3b55802f38 100644
--- a/InvenTree/templates/js/translated/forms.js
+++ b/InvenTree/templates/js/translated/forms.js
@@ -240,6 +240,7 @@ function constructDeleteForm(fields, options) {
* - hidden: Set to true to hide the field
* - icon: font-awesome icon to display before the field
* - prefix: Custom HTML prefix to display before the field
+ * - data: map of data to fill out field values with
* - focus: Name of field to focus on when modal is displayed
* - preventClose: Set to true to prevent form from closing on success
* - onSuccess: callback function when form action is successful
@@ -263,6 +264,11 @@ function constructForm(url, options) {
// Default HTTP method
options.method = options.method || 'PATCH';
+ // Construct an "empty" data object if not provided
+ if (!options.data) {
+ options.data = {};
+ }
+
// Request OPTIONS endpoint from the API
getApiEndpointOptions(url, function(OPTIONS) {
@@ -346,10 +352,19 @@ function constructFormBody(fields, options) {
// otherwise *all* fields will be displayed
var displayed_fields = options.fields || fields;
+ // Handle initial data overrides
+ if (options.data) {
+ for (const field in options.data) {
+
+ if (field in fields) {
+ fields[field].value = options.data[field];
+ }
+ }
+ }
+
// Provide each field object with its own name
for(field in fields) {
fields[field].name = field;
-
// If any "instance_filters" are defined for the endpoint, copy them across (overwrite)
if (fields[field].instance_filters) {
From 2cb0b448b77b2dd429167c38f4d4ec60e37a1871 Mon Sep 17 00:00:00 2001
From: Oliver Walters
Date: Thu, 5 Aug 2021 00:15:55 +1000
Subject: [PATCH 18/28] Fix error message styles for API errors
- django ValidationError uses "__all__" key for non_field_errors
- whyyyyyyyyyyyy
---
InvenTree/InvenTree/serializers.py | 16 ++++++++++++---
InvenTree/templates/js/translated/part.js | 25 +++++++++++++++++++++++
2 files changed, 38 insertions(+), 3 deletions(-)
diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py
index 58d33697b7..baf08e112b 100644
--- a/InvenTree/InvenTree/serializers.py
+++ b/InvenTree/InvenTree/serializers.py
@@ -85,8 +85,10 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
"""
def __init__(self, instance=None, data=empty, **kwargs):
-
- # self.instance = instance
+ """
+ Custom __init__ routine to ensure that *default* values (as specified in the ORM)
+ are used by the DRF serializers, *if* the values are not provided by the user.
+ """
# If instance is None, we are creating a new instance
if instance is None and data is not empty:
@@ -193,7 +195,15 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
try:
instance.full_clean()
except (ValidationError, DjangoValidationError) as exc:
- raise ValidationError(detail=serializers.as_serializer_error(exc))
+
+ data = exc.message_dict
+
+ # Change '__all__' key (django style) to 'non_field_errors' (DRF style)
+ if '__all__' in data:
+ data['non_field_errors'] = data['__all__']
+ del data['__all__']
+
+ raise ValidationError(data)
return data
diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js
index 988481d77c..f8b410c9c0 100644
--- a/InvenTree/templates/js/translated/part.js
+++ b/InvenTree/templates/js/translated/part.js
@@ -173,7 +173,32 @@ function editPart(pk, options={}) {
title: '{% trans "Edit Part" %}',
reload: true,
});
+}
+
+function duplicatePart(pk, options={}) {
+
+ // First we need all the part information
+ inventreeGet(`/api/part/${pk}/`, {}, {
+
+ success: function(response) {
+
+ var fields = partFields({
+ duplicate: true
+ });
+
+ constructForm('{% url "api-part-list" %}', {
+ method: 'POST',
+ fields: fields,
+ title: '{% trans "Duplicate Part" %}',
+ data: response,
+ onSuccess: function(data) {
+ // Follow the new part
+ location.href = `/part/${data.pk}/`;
+ }
+ });
+ }
+ });
}
From 0e8fb6a5ad6df073770c243f9af96336f920365f Mon Sep 17 00:00:00 2001
From: Oliver Walters
Date: Thu, 5 Aug 2021 00:16:42 +1000
Subject: [PATCH 19/28] Refactored DuplicatePart form
- API endpoint now takes care of duplication of other data
---
InvenTree/part/api.py | 28 +++++
InvenTree/part/templates/part/part_base.html | 7 +-
InvenTree/part/urls.py | 1 -
InvenTree/part/views.py | 124 -------------------
InvenTree/templates/js/translated/part.js | 15 ++-
5 files changed, 43 insertions(+), 132 deletions(-)
diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py
index 88866ad58c..789ba9b9b7 100644
--- a/InvenTree/part/api.py
+++ b/InvenTree/part/api.py
@@ -651,6 +651,34 @@ class PartList(generics.ListCreateAPIView):
part.save(**{'add_category_templates': copy_templates})
+ # Optionally copy data from another part (e.g. when duplicating)
+ copy_from = request.data.get('copy_from', None)
+
+ if copy_from is not None:
+
+ try:
+ original = Part.objects.get(pk=copy_from)
+
+ copy_bom = str2bool(request.data.get('copy_bom', False))
+ copy_parameters = str2bool(request.data.get('copy_parameters', False))
+ copy_image = str2bool(request.data.get('copy_image', True))
+
+ # Copy image?
+ if copy_image:
+ part.image = original.image
+ part.save()
+
+ # Copy BOM?
+ if copy_bom:
+ part.copy_bom_from(original)
+
+ # Copy parameter data?
+ if copy_parameters:
+ part.copy_parameters_from(original)
+
+ except (ValueError, Part.DoesNotExist):
+ pass
+
# Optionally create initial stock item
try:
initial_stock = Decimal(request.data.get('initial_stock', 0))
diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html
index ec637412a8..0c29f1c26b 100644
--- a/InvenTree/part/templates/part/part_base.html
+++ b/InvenTree/part/templates/part/part_base.html
@@ -486,12 +486,7 @@
{% if roles.part.add %}
$("#part-duplicate").click(function() {
- launchModalForm(
- "{% url 'part-duplicate' part.id %}",
- {
- follow: true,
- }
- );
+ duplicatePart({{ part.pk }});
});
{% endif %}
diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py
index 0802a94f1a..53d28f7ccb 100644
--- a/InvenTree/part/urls.py
+++ b/InvenTree/part/urls.py
@@ -40,7 +40,6 @@ part_detail_urls = [
url(r'^bom-export/?', views.BomExport.as_view(), name='bom-export'),
url(r'^bom-download/?', views.BomDownload.as_view(), name='bom-download'),
url(r'^validate-bom/', views.BomValidate.as_view(), name='bom-validate'),
- url(r'^duplicate/', views.PartDuplicate.as_view(), name='part-duplicate'),
url(r'^make-variant/', views.MakePartVariant.as_view(), name='make-part-variant'),
url(r'^pricing/', views.PartPricing.as_view(), name='part-pricing'),
diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py
index 3e4b6c59d7..c4ae2aee77 100644
--- a/InvenTree/part/views.py
+++ b/InvenTree/part/views.py
@@ -314,130 +314,6 @@ class MakePartVariant(AjaxCreateView):
return initials
-class PartDuplicate(AjaxCreateView):
- """ View for duplicating an existing Part object.
-
- - Part is provided in the URL '/part//copy/'
- - Option for 'deep-copy' which will duplicate all BOM items (default = True)
- """
-
- model = Part
- form_class = part_forms.EditPartForm
-
- ajax_form_title = _("Duplicate Part")
- ajax_template_name = "part/copy_part.html"
-
- def get_data(self):
- return {
- 'success': _('Copied part')
- }
-
- def get_part_to_copy(self):
- try:
- return Part.objects.get(id=self.kwargs['pk'])
- except (Part.DoesNotExist, ValueError):
- return None
-
- def get_context_data(self):
- return {
- 'part': self.get_part_to_copy()
- }
-
- def get_form(self):
- form = super(AjaxCreateView, self).get_form()
-
- # Force display of the 'bom_copy' widget
- form.fields['bom_copy'].widget = CheckboxInput()
-
- # Force display of the 'parameters_copy' widget
- form.fields['parameters_copy'].widget = CheckboxInput()
-
- return form
-
- def post(self, request, *args, **kwargs):
- """ Capture the POST request for part duplication
-
- - If the bom_copy object is set, copy all the BOM items too!
- - If the parameters_copy object is set, copy all the parameters too!
- """
-
- form = self.get_form()
-
- context = self.get_context_data()
-
- valid = form.is_valid()
-
- name = request.POST.get('name', None)
-
- if name:
- matches = match_part_names(name)
-
- if len(matches) > 0:
- # Display the first five closest matches
- context['matches'] = matches[:5]
-
- # Enforce display of the checkbox
- form.fields['confirm_creation'].widget = CheckboxInput()
-
- # Check if the user has checked the 'confirm_creation' input
- confirmed = str2bool(request.POST.get('confirm_creation', False))
-
- if not confirmed:
- msg = _('Possible matches exist - confirm creation of new part')
- form.add_error('confirm_creation', msg)
- form.pre_form_warning = msg
- valid = False
-
- data = {
- 'form_valid': valid
- }
-
- if valid:
- # Create the new Part
- part = form.save(commit=False)
-
- part.creation_user = request.user
- part.save()
-
- data['pk'] = part.pk
- data['text'] = str(part)
-
- bom_copy = str2bool(request.POST.get('bom_copy', False))
- parameters_copy = str2bool(request.POST.get('parameters_copy', False))
-
- original = self.get_part_to_copy()
-
- if original:
- part.deep_copy(original, bom=bom_copy, parameters=parameters_copy)
-
- try:
- data['url'] = part.get_absolute_url()
- except AttributeError:
- pass
-
- if valid:
- pass
-
- return self.renderJsonResponse(request, form, data, context=context)
-
- def get_initial(self):
- """ Get initial data based on the Part to be copied from.
- """
-
- part = self.get_part_to_copy()
-
- if part:
- initials = model_to_dict(part)
- else:
- initials = super(AjaxCreateView, self).get_initial()
-
- initials['bom_copy'] = str2bool(InvenTreeSetting.get_setting('PART_COPY_BOM', True))
-
- initials['parameters_copy'] = str2bool(InvenTreeSetting.get_setting('PART_COPY_PARAMETERS', True))
-
- return initials
-
-
class PartImport(FileManagementFormView):
''' Part: Upload file, match to fields and import parts(using multi-Step form) '''
permission_required = 'part.add'
diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js
index f8b410c9c0..a1d40f7bf4 100644
--- a/InvenTree/templates/js/translated/part.js
+++ b/InvenTree/templates/js/translated/part.js
@@ -110,6 +110,19 @@ function partFields(options={}) {
html: `
{% trans "Part Duplication Options" %}
`,
};
+ fields.copy_from = {
+ type: 'integer',
+ hidden: true,
+ value: options.duplicate,
+ },
+
+ fields.copy_image = {
+ type: 'boolean',
+ label: '{% trans "Copy Image" %}',
+ help_text: '{% trans "Copy image from original part" %}',
+ value: true,
+ },
+
fields.copy_bom = {
type: 'boolean',
label: '{% trans "Copy BOM" %}',
@@ -184,7 +197,7 @@ function duplicatePart(pk, options={}) {
success: function(response) {
var fields = partFields({
- duplicate: true
+ duplicate: pk,
});
constructForm('{% url "api-part-list" %}', {
From aa4ed9feb07c1f32ab45e99f3d0de8e6aa2870ee Mon Sep 17 00:00:00 2001
From: Oliver Walters
Date: Thu, 5 Aug 2021 00:24:38 +1000
Subject: [PATCH 20/28] Refactor MakeVariant form
- Now is essentially identical to the DuplicatePart form
- Uses the API form structure
---
InvenTree/part/forms.py | 76 ---------------------
InvenTree/part/templates/part/detail.html | 7 +-
InvenTree/part/test_views.py | 19 ------
InvenTree/part/urls.py | 2 +-
InvenTree/part/views.py | 81 -----------------------
InvenTree/templates/js/translated/part.js | 12 +++-
6 files changed, 15 insertions(+), 182 deletions(-)
diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py
index 1fc2848440..f5d7d39266 100644
--- a/InvenTree/part/forms.py
+++ b/InvenTree/part/forms.py
@@ -177,82 +177,6 @@ class SetPartCategoryForm(forms.Form):
part_category = TreeNodeChoiceField(queryset=PartCategory.objects.all(), required=True, help_text=_('Select part category'))
-class EditPartForm(HelperForm):
- """
- Form for editing a Part object.
- """
-
- field_prefix = {
- 'keywords': 'fa-key',
- 'link': 'fa-link',
- 'IPN': 'fa-hashtag',
- 'default_expiry': 'fa-stopwatch',
- }
-
- bom_copy = forms.BooleanField(required=False,
- initial=True,
- help_text=_("Duplicate all BOM data for this part"),
- label=_('Copy BOM'),
- widget=forms.HiddenInput())
-
- parameters_copy = forms.BooleanField(required=False,
- initial=True,
- help_text=_("Duplicate all parameter data for this part"),
- label=_('Copy Parameters'),
- widget=forms.HiddenInput())
-
- confirm_creation = forms.BooleanField(required=False,
- initial=False,
- help_text=_('Confirm part creation'),
- widget=forms.HiddenInput())
-
- selected_category_templates = forms.BooleanField(required=False,
- initial=False,
- label=_('Include category parameter templates'),
- widget=forms.HiddenInput())
-
- parent_category_templates = forms.BooleanField(required=False,
- initial=False,
- label=_('Include parent categories parameter templates'),
- widget=forms.HiddenInput())
-
- initial_stock = forms.IntegerField(required=False,
- initial=0,
- label=_('Initial stock amount'),
- help_text=_('Create stock for this part'))
-
- class Meta:
- model = Part
- fields = [
- 'confirm_creation',
- 'category',
- 'selected_category_templates',
- 'parent_category_templates',
- 'name',
- 'IPN',
- 'description',
- 'revision',
- 'bom_copy',
- 'parameters_copy',
- 'keywords',
- 'variant_of',
- 'link',
- 'default_location',
- 'default_supplier',
- 'default_expiry',
- 'units',
- 'minimum_stock',
- 'initial_stock',
- 'component',
- 'assembly',
- 'is_template',
- 'trackable',
- 'purchaseable',
- 'salable',
- 'virtual',
- ]
-
-
class EditPartParameterTemplateForm(HelperForm):
""" Form for editing a PartParameterTemplate object """
diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html
index 267b880d49..165ea37e66 100644
--- a/InvenTree/part/templates/part/detail.html
+++ b/InvenTree/part/templates/part/detail.html
@@ -525,10 +525,11 @@
loadPartVariantTable($('#variants-table'), {{ part.pk }});
$('#new-variant').click(function() {
- launchModalForm(
- "{% url 'make-part-variant' part.id %}",
+
+ duplicatePart(
+ {{ part.pk}},
{
- follow: true,
+ variant: true,
}
);
});
diff --git a/InvenTree/part/test_views.py b/InvenTree/part/test_views.py
index c555687183..5f2a9b1583 100644
--- a/InvenTree/part/test_views.py
+++ b/InvenTree/part/test_views.py
@@ -155,25 +155,6 @@ class PartDetailTest(PartViewTestCase):
self.assertIn('streaming_content', dir(response))
-class PartTests(PartViewTestCase):
- """ Tests for Part forms """
-
- def test_part_duplicate(self):
- """ Launch form to duplicate part """
-
- # First try with an invalid part
- response = self.client.get(reverse('part-duplicate', args=(9999,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
- self.assertEqual(response.status_code, 200)
-
- response = self.client.get(reverse('part-duplicate', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
- self.assertEqual(response.status_code, 200)
-
- def test_make_variant(self):
-
- response = self.client.get(reverse('make-part-variant', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
- self.assertEqual(response.status_code, 200)
-
-
class PartRelatedTests(PartViewTestCase):
def test_valid_create(self):
diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py
index 53d28f7ccb..13fc6f7c16 100644
--- a/InvenTree/part/urls.py
+++ b/InvenTree/part/urls.py
@@ -40,7 +40,7 @@ part_detail_urls = [
url(r'^bom-export/?', views.BomExport.as_view(), name='bom-export'),
url(r'^bom-download/?', views.BomDownload.as_view(), name='bom-download'),
url(r'^validate-bom/', views.BomValidate.as_view(), name='bom-validate'),
- url(r'^make-variant/', views.MakePartVariant.as_view(), name='make-part-variant'),
+
url(r'^pricing/', views.PartPricing.as_view(), name='part-pricing'),
url(r'^bom-upload/?', views.BomUpload.as_view(), name='upload-bom'),
diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py
index c4ae2aee77..e805e8f260 100644
--- a/InvenTree/part/views.py
+++ b/InvenTree/part/views.py
@@ -233,87 +233,6 @@ class PartSetCategory(AjaxUpdateView):
return ctx
-class MakePartVariant(AjaxCreateView):
- """ View for creating a new variant based on an existing template Part
-
- - Part is provided in the URL '/part//make_variant/'
- - Automatically copy relevent data (BOM, etc, etc)
-
- """
-
- model = Part
- form_class = part_forms.EditPartForm
-
- ajax_form_title = _('Create Variant')
- ajax_template_name = 'part/variant_part.html'
-
- def get_part_template(self):
- return get_object_or_404(Part, id=self.kwargs['pk'])
-
- def get_context_data(self):
- return {
- 'part': self.get_part_template(),
- }
-
- def get_form(self):
- form = super(AjaxCreateView, self).get_form()
-
- # Hide some variant-related fields
- # form.fields['variant_of'].widget = HiddenInput()
-
- # Force display of the 'bom_copy' widget
- form.fields['bom_copy'].widget = CheckboxInput()
-
- # Force display of the 'parameters_copy' widget
- form.fields['parameters_copy'].widget = CheckboxInput()
-
- return form
-
- def post(self, request, *args, **kwargs):
-
- form = self.get_form()
- context = self.get_context_data()
- part_template = self.get_part_template()
-
- valid = form.is_valid()
-
- data = {
- 'form_valid': valid,
- }
-
- if valid:
- # Create the new part variant
- part = form.save(commit=False)
- part.variant_of = part_template
- part.is_template = False
-
- part.save()
-
- data['pk'] = part.pk
- data['text'] = str(part)
- data['url'] = part.get_absolute_url()
-
- bom_copy = str2bool(request.POST.get('bom_copy', False))
- parameters_copy = str2bool(request.POST.get('parameters_copy', False))
-
- # Copy relevent information from the template part
- part.deep_copy(part_template, bom=bom_copy, parameters=parameters_copy)
-
- return self.renderJsonResponse(request, form, data, context=context)
-
- def get_initial(self):
-
- part_template = self.get_part_template()
-
- initials = model_to_dict(part_template)
- initials['is_template'] = False
- initials['variant_of'] = part_template
- initials['bom_copy'] = InvenTreeSetting.get_setting('PART_COPY_BOM')
- initials['parameters_copy'] = InvenTreeSetting.get_setting('PART_COPY_PARAMETERS')
-
- return initials
-
-
class PartImport(FileManagementFormView):
''' Part: Upload file, match to fields and import parts(using multi-Step form) '''
permission_required = 'part.add'
diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js
index a1d40f7bf4..3def7abdad 100644
--- a/InvenTree/templates/js/translated/part.js
+++ b/InvenTree/templates/js/translated/part.js
@@ -189,22 +189,30 @@ function editPart(pk, options={}) {
}
+// Launch form to duplicate a part
function duplicatePart(pk, options={}) {
// First we need all the part information
inventreeGet(`/api/part/${pk}/`, {}, {
- success: function(response) {
+ success: function(data) {
var fields = partFields({
duplicate: pk,
});
+
+ // If we are making a "variant" part
+ if (options.variant) {
+
+ // Override the "variant_of" field
+ data.variant_of = pk;
+ }
constructForm('{% url "api-part-list" %}', {
method: 'POST',
fields: fields,
title: '{% trans "Duplicate Part" %}',
- data: response,
+ data: data,
onSuccess: function(data) {
// Follow the new part
location.href = `/part/${data.pk}/`;
From dd78464a749c51d44f588f320ce7ad9f196a7c59 Mon Sep 17 00:00:00 2001
From: Oliver Walters
Date: Thu, 5 Aug 2021 00:25:47 +1000
Subject: [PATCH 21/28] remove unused function
---
InvenTree/part/models.py | 51 -------------------------------------
InvenTree/part/test_part.py | 8 +-----
InvenTree/part/views.py | 4 +--
3 files changed, 2 insertions(+), 61 deletions(-)
diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py
index b75edde9cc..89e92115ca 100644
--- a/InvenTree/part/models.py
+++ b/InvenTree/part/models.py
@@ -235,57 +235,6 @@ def rename_part_image(instance, filename):
return os.path.join(base, fname)
-def match_part_names(match, threshold=80, reverse=True, compare_length=False):
- """ Return a list of parts whose name matches the search term using fuzzy search.
-
- Args:
- match: Term to match against
- threshold: Match percentage that must be exceeded (default = 65)
- reverse: Ordering for search results (default = True - highest match is first)
- compare_length: Include string length checks
-
- Returns:
- A sorted dict where each element contains the following key:value pairs:
- - 'part' : The matched part
- - 'ratio' : The matched ratio
- """
-
- match = str(match).strip().lower()
-
- if len(match) == 0:
- return []
-
- parts = Part.objects.all()
-
- matches = []
-
- for part in parts:
- compare = str(part.name).strip().lower()
-
- if len(compare) == 0:
- continue
-
- ratio = fuzz.partial_token_sort_ratio(compare, match)
-
- if compare_length:
- # Also employ primitive length comparison
- # TODO - Improve this somewhat...
- l_min = min(len(match), len(compare))
- l_max = max(len(match), len(compare))
-
- ratio *= (l_min / l_max)
-
- if ratio >= threshold:
- matches.append({
- 'part': part,
- 'ratio': round(ratio, 1)
- })
-
- matches = sorted(matches, key=lambda item: item['ratio'], reverse=reverse)
-
- return matches
-
-
class PartManager(TreeManager):
"""
Defines a custom object manager for the Part model.
diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py
index e30c80549f..b32b30a22e 100644
--- a/InvenTree/part/test_part.py
+++ b/InvenTree/part/test_part.py
@@ -12,7 +12,7 @@ from django.core.exceptions import ValidationError
import os
from .models import Part, PartCategory, PartTestTemplate
-from .models import rename_part_image, match_part_names
+from .models import rename_part_image
from .templatetags import inventree_extras
import part.settings
@@ -163,12 +163,6 @@ class PartTest(TestCase):
def test_copy(self):
self.r2.deep_copy(self.r1, image=True, bom=True)
- def test_match_names(self):
-
- matches = match_part_names('M2x5 LPHS')
-
- self.assertTrue(len(matches) > 0)
-
def test_sell_pricing(self):
# check that the sell pricebreaks were loaded
self.assertTrue(self.r1.has_price_breaks)
diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py
index e805e8f260..0e06678694 100644
--- a/InvenTree/part/views.py
+++ b/InvenTree/part/views.py
@@ -14,8 +14,7 @@ from django.shortcuts import HttpResponseRedirect
from django.utils.translation import gettext_lazy as _
from django.urls import reverse
from django.views.generic import DetailView, ListView
-from django.forms.models import model_to_dict
-from django.forms import HiddenInput, CheckboxInput
+from django.forms import HiddenInput
from django.conf import settings
from django.contrib import messages
@@ -35,7 +34,6 @@ from .models import PartCategory, Part, PartRelated
from .models import PartParameterTemplate
from .models import PartCategoryParameterTemplate
from .models import BomItem
-from .models import match_part_names
from .models import PartSellPriceBreak, PartInternalPriceBreak
from common.models import InvenTreeSetting
From aaf394ca7a1698ea6839fb7511485a1f85480f9f Mon Sep 17 00:00:00 2001
From: Oliver Walters
Date: Thu, 5 Aug 2021 00:26:21 +1000
Subject: [PATCH 22/28] PEP fixes
---
InvenTree/part/models.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py
index 89e92115ca..28fd3ce793 100644
--- a/InvenTree/part/models.py
+++ b/InvenTree/part/models.py
@@ -34,7 +34,6 @@ from stdimage.models import StdImageField
from decimal import Decimal, InvalidOperation
from datetime import datetime
-from rapidfuzz import fuzz
import hashlib
from InvenTree import helpers
From 6acff2a26e8f6a578207d16ef41b61bf1b04f415 Mon Sep 17 00:00:00 2001
From: Oliver Walters
Date: Thu, 5 Aug 2021 00:40:02 +1000
Subject: [PATCH 23/28] Fixes unit test
---
InvenTree/part/test_part.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py
index b32b30a22e..1e831601a4 100644
--- a/InvenTree/part/test_part.py
+++ b/InvenTree/part/test_part.py
@@ -287,7 +287,7 @@ class PartSettingsTest(TestCase):
part = self.make_part()
self.assertTrue(part.component)
- self.assertFalse(part.purchaseable)
+ self.assertTrue(part.purchaseable)
self.assertFalse(part.salable)
self.assertFalse(part.trackable)
From 655e5692e98d55c12dd122d8513ef3346199e9da Mon Sep 17 00:00:00 2001
From: Oliver Walters
Date: Thu, 5 Aug 2021 00:58:07 +1000
Subject: [PATCH 24/28] More unit test fixes
---
InvenTree/part/test_part.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py
index 1e831601a4..1bd9fdf87d 100644
--- a/InvenTree/part/test_part.py
+++ b/InvenTree/part/test_part.py
@@ -275,7 +275,7 @@ class PartSettingsTest(TestCase):
"""
self.assertTrue(part.settings.part_component_default())
- self.assertFalse(part.settings.part_purchaseable_default())
+ self.assertTrue(part.settings.part_purchaseable_default())
self.assertFalse(part.settings.part_salable_default())
self.assertFalse(part.settings.part_trackable_default())
From c7712d4235ef06ade7baf29f40bfdc77706dc859 Mon Sep 17 00:00:00 2001
From: Oliver Walters
Date: Thu, 5 Aug 2021 01:13:48 +1000
Subject: [PATCH 25/28] even more unit tests
---
InvenTree/part/test_api.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py
index 7700c5c61f..bbd73b73e0 100644
--- a/InvenTree/part/test_api.py
+++ b/InvenTree/part/test_api.py
@@ -434,8 +434,8 @@ class PartAPITest(InvenTreeAPITestCase):
self.assertTrue(data['active'])
self.assertFalse(data['virtual'])
- # By default, parts are not purchaseable
- self.assertFalse(data['purchaseable'])
+ # By default, parts are purchaseable
+ self.assertTrue(data['purchaseable'])
# Set the default 'purchaseable' status to True
InvenTreeSetting.set_setting(
From c0ccb8f588634f07a94209eea2c1dc58a9002c61 Mon Sep 17 00:00:00 2001
From: eeintech
Date: Wed, 4 Aug 2021 17:11:35 -0400
Subject: [PATCH 26/28] Fixed typo for build responsible column header
---
InvenTree/templates/js/translated/build.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js
index 4b8cd47eb5..26f3876af3 100644
--- a/InvenTree/templates/js/translated/build.js
+++ b/InvenTree/templates/js/translated/build.js
@@ -927,7 +927,7 @@ function loadBuildTable(table, options) {
},
{
field: 'responsible',
- title: '{% trans "Resposible" %}',
+ title: '{% trans "Responsible" %}',
sortable: true,
formatter: function(value, row, index, field) {
if (value)
From fa6daeb679a1beaea293dc9b90ce22031f0c3163 Mon Sep 17 00:00:00 2001
From: Oliver
Date: Thu, 5 Aug 2021 09:47:15 +1000
Subject: [PATCH 27/28] Pin weasyprint version to 52.5
---
requirements.txt | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/requirements.txt b/requirements.txt
index abcf2cb098..839237c6a1 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -21,7 +21,8 @@ coverage==5.3 # Unit test coverage
coveralls==2.1.2 # Coveralls linking (for Travis)
rapidfuzz==0.7.6 # Fuzzy string matching
django-stdimage==5.1.1 # Advanced ImageField management
-django-weasyprint==1.0.1 # HTML PDF export
+weasyprint==52.5 # PDF generation library (Note: in the future need to update to 53)
+django-weasyprint==1.0.1 # django weasyprint integration
django-debug-toolbar==2.2 # Debug / profiling toolbar
django-admin-shell==0.1.2 # Python shell for the admin interface
py-moneyed==0.8.0 # Specific version requirement for py-moneyed
From 00ffab472c7fbfe368a463ccfa4d99fd606d5674 Mon Sep 17 00:00:00 2001
From: Oliver
Date: Thu, 5 Aug 2021 10:44:47 +1000
Subject: [PATCH 28/28] Fix for build report template
---
.../report/templates/report/inventree_build_order_base.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/InvenTree/report/templates/report/inventree_build_order_base.html b/InvenTree/report/templates/report/inventree_build_order_base.html
index 5a0cbde93c..2d2d2766bb 100644
--- a/InvenTree/report/templates/report/inventree_build_order_base.html
+++ b/InvenTree/report/templates/report/inventree_build_order_base.html
@@ -79,7 +79,7 @@ content: "v{{report_revision}} - {{ date.isoformat }}";
{% block header_content %}
-
+