From ac2797c7a1b48de752558d500d452889349893b5 Mon Sep 17 00:00:00 2001 From: eeintech Date: Mon, 16 Nov 2020 16:10:00 -0500 Subject: [PATCH 1/4] Improved API endpoint for category parameter templates --- InvenTree/part/api.py | 39 ++++++++++++++++++++++++----------- InvenTree/part/serializers.py | 6 ++---- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index a258d58962..9bf7294335 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -113,9 +113,9 @@ class CategoryDetail(generics.RetrieveUpdateDestroyAPIView): class CategoryParameters(generics.ListAPIView): - """ API endpoint for accessing a list of PartCategory objects. + """ API endpoint for accessing a list of PartCategoryParameterTemplate objects. - - GET: Return a list of PartCategory objects + - GET: Return a list of PartCategoryParameterTemplate objects """ queryset = PartCategoryParameterTemplate.objects.all() @@ -124,21 +124,36 @@ class CategoryParameters(generics.ListAPIView): def get_queryset(self): """ Custom filtering: - - Allow filtering by "null" parent to retrieve top-level part categories + - Allow filtering by "null" parent to retrieve all categories parameter templates + - Allow filtering by category + - Allow traversing all parent categories """ - cat_id = self.kwargs.get('pk', None) + try: + cat_id = int(self.request.query_params.get('category', None)) + except TypeError: + cat_id = None + fetch_parent = str2bool(self.request.query_params.get('fetch_parent', 'true')) queryset = super().get_queryset() - if cat_id is not None: - - try: - cat_id = int(cat_id) - queryset = queryset.filter(category=cat_id) - except ValueError: - pass + if isinstance(cat_id, int): + try: + category = PartCategory.objects.get(pk=cat_id) + except PartCategory.DoesNotExist: + # Return empty queryset + return PartCategoryParameterTemplate.objects.none() + + category_list = [cat_id] + + if fetch_parent: + parent_categories = category.get_ancestors() + for parent in parent_categories: + category_list.append(parent.pk) + + queryset = queryset.filter(category__in=category_list) + return queryset @@ -895,8 +910,8 @@ part_api_urls = [ # Base URL for PartCategory API endpoints url(r'^category/', include([ - url(r'^(?P\d+)/parameters/?', CategoryParameters.as_view(), name='api-part-category-parameters'), url(r'^(?P\d+)/?', CategoryDetail.as_view(), name='api-part-category-detail'), + url(r'^parameters/?', CategoryParameters.as_view(), name='api-part-category-parameters'), url(r'^$', CategoryList.as_view(), name='api-part-category-list'), ])), diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index bda49da671..7bec904ce0 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -423,9 +423,8 @@ class PartParameterTemplateSerializer(InvenTreeModelSerializer): class CategoryParameterTemplateSerializer(InvenTreeModelSerializer): """ Serializer for PartCategoryParameterTemplate """ - parameter_template_detail = PartParameterTemplateSerializer(source='parameter_template', - many=False, - read_only=True) + parameter_template = PartParameterTemplateSerializer(many=False, + read_only=True) class Meta: model = PartCategoryParameterTemplate @@ -433,6 +432,5 @@ class CategoryParameterTemplateSerializer(InvenTreeModelSerializer): 'pk', 'category', 'parameter_template', - 'parameter_template_detail', 'default_value', ] From 6144d7e209b07bc01153b05538f60396639b212c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 17 Nov 2020 15:29:44 +1100 Subject: [PATCH 2/4] Fix for StockItem creation form - purchase_price field was required (should not be!) - Fixed some validation issues - Cleaned up form implementation --- InvenTree/InvenTree/views.py | 15 +- InvenTree/stock/forms.py | 2 +- .../migrations/0055_auto_20201117_1453.py | 19 ++ InvenTree/stock/models.py | 1 + InvenTree/stock/views.py | 188 ++++++++---------- 5 files changed, 110 insertions(+), 115 deletions(-) create mode 100644 InvenTree/stock/migrations/0055_auto_20201117_1453.py diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index e61e2d497c..b2ef8f965a 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -388,14 +388,15 @@ class AjaxCreateView(AjaxMixin, CreateView): # Save the object to the database self.object = self.save(self.form) - # Return the PK of the newly-created object - data['pk'] = self.object.pk - data['text'] = str(self.object) + if self.object: + # Return the PK of the newly-created object + data['pk'] = self.object.pk + data['text'] = str(self.object) - try: - data['url'] = self.object.get_absolute_url() - except AttributeError: - pass + try: + data['url'] = self.object.get_absolute_url() + except AttributeError: + pass return self.renderJsonResponse(request, self.form, data) diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index 77d0f58295..8ab88155e2 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -124,11 +124,11 @@ class CreateStockItemForm(HelperForm): fields = [ 'part', 'supplier_part', - 'purchase_price', 'location', 'quantity', 'batch', 'serial_numbers', + 'purchase_price', 'link', 'delete_on_deplete', 'status', diff --git a/InvenTree/stock/migrations/0055_auto_20201117_1453.py b/InvenTree/stock/migrations/0055_auto_20201117_1453.py new file mode 100644 index 0000000000..265347ae5c --- /dev/null +++ b/InvenTree/stock/migrations/0055_auto_20201117_1453.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0.7 on 2020-11-17 03:53 + +from django.db import migrations +import djmoney.models.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0054_remove_stockitem_build_order'), + ] + + operations = [ + migrations.AlterField( + model_name='stockitem', + name='purchase_price', + field=djmoney.models.fields.MoneyField(blank=True, decimal_places=4, default_currency='USD', help_text='Single unit purchase price at time of purchase', max_digits=19, null=True, verbose_name='Purchase Price'), + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 01b7b27cc4..d1e46c53a7 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -453,6 +453,7 @@ class StockItem(MPTTModel): max_digits=19, decimal_places=4, default_currency='USD', + blank=True, null=True, verbose_name=_('Purchase Price'), help_text=_('Single unit purchase price at time of purchase'), diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 88c2274ddc..175a06ae80 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -1607,127 +1607,101 @@ class StockItemCreate(AjaxCreateView): return initials - def post(self, request, *args, **kwargs): - """ Handle POST of StockItemCreate form. - - - Manage serial-number valdiation for tracked parts + def validate(self, item, form): + """ + Extra form validation steps """ - part = None + data = form.cleaned_data - form = self.get_form() + part = data['part'] + + quantity = data.get('quantity', None) - data = {} + if not quantity: + return - valid = form.is_valid() + try: + quantity = Decimal(quantity) + except (ValueError, InvalidOperation): + form.add_error('quantity', _('Invalid quantity provided')) + return - if valid: - part_id = form['part'].value() - try: - part = Part.objects.get(id=part_id) - quantity = Decimal(form['quantity'].value()) + if quantity < 0: + form.add_error('quantity', _('Quantity cannot be negative')) - except (Part.DoesNotExist, ValueError, InvalidOperation): - part = None - quantity = 1 - valid = False - form.add_error('quantity', _('Invalid quantity')) + # Trackable parts are treated differently + if part.trackable: + sn = data.get('serial_numbers', '') + sn = str(sn).strip() - if quantity < 0: - form.add_error('quantity', _('Quantity cannot be less than zero')) - valid = False + if len(sn) > 0: + serials = extract_serial_numbers(sn, quantity) - if part is None: - form.add_error('part', _('Invalid part selection')) + existing = part.find_conflicting_serial_numbers(serials) + + if len(existing) > 0: + exists = ','.join([str(x) for x in existing]) + + form.add_error( + 'serial_numbers', + _('Serial numbers already exist') + ': ' + exists + ) + + def save(self, form, **kwargs): + """ + Create a new StockItem based on the provided form data. + """ + + data = form.cleaned_data + + part = data['part'] + + quantity = data['quantity'] + + if part.trackable: + sn = data.get('serial_numbers', '') + sn = str(sn).strip() + + # Create a single stock item for each provided serial number + if len(sn) > 0: + serials = extract_serial_numbers(sn, quantity) + + for serial in serials: + item = StockItem( + part=part, + quantity=1, + serial=serial, + supplier_part=data.get('supplier_part', None), + location=data.get('location', None), + batch=data.get('batch', None), + delete_on_deplete=False, + status=data.get('status'), + link=data.get('link', ''), + ) + + item.save(user=self.request.user) + + # Create a single StockItem of the specified quantity else: - # A trackable part must provide serial numbesr - if part.trackable: - sn = request.POST.get('serial_numbers', '') + form._post_clean() - sn = str(sn).strip() + item = form.save(commit=False) + item.user = self.request.user + item.save() - # If user has specified a range of serial numbers - if len(sn) > 0: - try: - serials = extract_serial_numbers(sn, quantity) + return item + + # Non-trackable part + else: - existing = part.find_conflicting_serial_numbers(serials) + form._post_clean() + + item = form.save(commit=False) + item.user = self.request.user + item.save() - if len(existing) > 0: - exists = ",".join([str(x) for x in existing]) - form.add_error( - 'serial_numbers', - _('Serial numbers already exist') + ': ' + exists - ) - valid = False - - else: - # At this point we have a list of serial numbers which we know are valid, - # and do not currently exist - form.clean() - - form_data = form.cleaned_data - - if form.is_valid(): - - for serial in serials: - # Create a new stock item for each serial number - item = StockItem( - part=part, - quantity=1, - serial=serial, - supplier_part=form_data.get('supplier_part'), - location=form_data.get('location'), - batch=form_data.get('batch'), - delete_on_deplete=False, - status=form_data.get('status'), - link=form_data.get('link'), - ) - - item.save(user=request.user) - - data['success'] = _('Created {n} new stock items'.format(n=len(serials))) - valid = True - - except ValidationError as e: - form.add_error('serial_numbers', e.messages) - valid = False - - else: - # We have a serialized part, but no serial numbers specified... - form.clean() - form._post_clean() - - if form.is_valid(): - - 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") - - valid = True - - else: # Referenced Part object is not marked as "trackable" - # For non-serialized items, simply save the form. - # We need to call _post_clean() here because it is prevented in the form implementation - form.clean() - form._post_clean() - - if form.is_valid: - 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") - - valid = True - - data['form_valid'] = valid and form.is_valid() - - return self.renderJsonResponse(request, form, data=data) + return item class StockLocationDelete(AjaxDeleteView): From 24fd520ec3225c76c5b36c64b79c6a92e047d90c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 18 Nov 2020 15:03:51 +1100 Subject: [PATCH 3/4] Smallt tweaks for the Stock table --- InvenTree/InvenTree/status_codes.py | 1 + InvenTree/templates/js/stock.js | 16 ++++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/InvenTree/InvenTree/status_codes.py b/InvenTree/InvenTree/status_codes.py index b527009ede..1d5335ad2f 100644 --- a/InvenTree/InvenTree/status_codes.py +++ b/InvenTree/InvenTree/status_codes.py @@ -182,6 +182,7 @@ class StockStatus(StatusCode): ATTENTION: 'yellow', DAMAGED: 'red', DESTROYED: 'red', + LOST: 'grey', REJECTED: 'red', } diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js index e59bf5d469..da57d4f048 100644 --- a/InvenTree/templates/js/stock.js +++ b/InvenTree/templates/js/stock.js @@ -536,13 +536,17 @@ function loadStockTable(table, options) { // Special stock status codes - // 65 = "REJECTED" - if (row.status == 65) { - html += makeIconButton('fa-times-circle icon-red', '{% trans "Stock item has been rejected" %}'); + // REJECTED + if (row.status == {{ StockStatus.REJECTED }}) { + console.log("REJECTED - {{ StockStatus.REJECTED }}"); + html += makeIconBadge('fa-times-circle icon-red', '{% trans "Stock item has been rejected" %}'); } - // 70 = "LOST" - else if (row.status == 70) { - html += makeIconButton('fa-question-circle','{% trans "Stock item is lost" %}'); + // LOST + else if (row.status == {{ StockStatus.LOST }}) { + html += makeIconBadge('fa-question-circle','{% trans "Stock item is lost" %}'); + } + else if (row.status == {{ StockStatus.DESTROYED }}) { + html += makeIconBadge('fa-skull-crossbones', '{% trans "Stock item is destroyed" %}'); } if (row.quantity <= 0) { From 599220a931e895e6b21b8dfe3dff90b63f12e0c8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 19 Nov 2020 09:15:16 +1100 Subject: [PATCH 4/4] Fixes for custom SQL queries - Don't use double quotes! - NO NO NO! - Single quotes only --- InvenTree/company/migrations/0026_auto_20201110_1011.py | 6 +++--- InvenTree/part/migrations/0056_auto_20201110_1125.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/InvenTree/company/migrations/0026_auto_20201110_1011.py b/InvenTree/company/migrations/0026_auto_20201110_1011.py index 24fac6f52d..1202fbe7f7 100644 --- a/InvenTree/company/migrations/0026_auto_20201110_1011.py +++ b/InvenTree/company/migrations/0026_auto_20201110_1011.py @@ -67,7 +67,7 @@ def migrate_currencies(apps, schema_editor): currency_code = remap.get(currency_id, 'USD') # Update the currency code - response = cursor.execute(f'UPDATE part_supplierpricebreak set price_currency= "{currency_code}" where id={pk};') + response = cursor.execute(f"UPDATE part_supplierpricebreak set price_currency= '{currency_code}' where id={pk};") count += 1 @@ -105,7 +105,7 @@ def reverse_currencies(apps, schema_editor): # For each currency code in use, check if we have a matching Currency object for code in codes_in_use: - response = cursor.execute(f'SELECT id, suffix from common_currency where suffix="{code}";') + response = cursor.execute(f"SELECT id, suffix from common_currency where suffix='{code}';") row = response.fetchone() if row is not None: @@ -133,7 +133,7 @@ def reverse_currencies(apps, schema_editor): # Update the table to point to the Currency objects print(f"Currency {suffix} -> pk {pk}") - response = cursor.execute(f'UPDATE part_supplierpricebreak set currency_id={pk} where price_currency="{suffix}";') + response = cursor.execute(f"UPDATE part_supplierpricebreak set currency_id={pk} where price_currency='{suffix}';") class Migration(migrations.Migration): diff --git a/InvenTree/part/migrations/0056_auto_20201110_1125.py b/InvenTree/part/migrations/0056_auto_20201110_1125.py index dff86173b6..3e4572b518 100644 --- a/InvenTree/part/migrations/0056_auto_20201110_1125.py +++ b/InvenTree/part/migrations/0056_auto_20201110_1125.py @@ -67,7 +67,7 @@ def migrate_currencies(apps, schema_editor): currency_code = remap.get(currency_id, 'USD') # Update the currency code - response = cursor.execute(f'UPDATE part_partsellpricebreak set price_currency= "{currency_code}" where id={pk};') + response = cursor.execute(f"UPDATE part_partsellpricebreak set price_currency='{currency_code}' where id={pk};") count += 1 @@ -105,7 +105,7 @@ def reverse_currencies(apps, schema_editor): # For each currency code in use, check if we have a matching Currency object for code in codes_in_use: - response = cursor.execute(f'SELECT id, suffix from common_currency where suffix="{code}";') + response = cursor.execute(f"SELECT id, suffix from common_currency where suffix='{code}';") row = response.fetchone() if row is not None: @@ -120,7 +120,7 @@ def reverse_currencies(apps, schema_editor): print(f"Creating new Currency object for {code}") # Construct a query to create a new Currency object - query = f'INSERT into common_currency (symbol, suffix, description, value, base) VALUES ("$", "{code}", "{description}", 1.0, False);' + query = f"INSERT into common_currency (symbol, suffix, description, value, base) VALUES ('$', '{code}', '{description}', 1.0, False);" response = cursor.execute(query) @@ -133,7 +133,7 @@ def reverse_currencies(apps, schema_editor): # Update the table to point to the Currency objects print(f"Currency {suffix} -> pk {pk}") - response = cursor.execute(f'UPDATE part_partsellpricebreak set currency_id={pk} where price_currency="{suffix}";') + response = cursor.execute(f"UPDATE part_partsellpricebreak set currency_id={pk} where price_currency='{suffix}';") class Migration(migrations.Migration):