From 7cc1c84b4b3f3613962ab7913870a477e292734b Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 20 Jun 2021 16:13:07 +1000 Subject: [PATCH 01/29] exclude exchange rate info from import / export tasks --- tasks.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tasks.py b/tasks.py index 3c4a2175e1..07fc64c03e 100644 --- a/tasks.py +++ b/tasks.py @@ -257,6 +257,8 @@ def content_excludes(): "django_q.task", "django_q.ormq", "users.owner", + "exchange.rate", + "exchange.exchangebackend", ] output = "" From 5a2227862a95333b1e741459ba131caf097ed315 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 20 Jun 2021 16:36:39 +1000 Subject: [PATCH 02/29] Exclude authtoken --- tasks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tasks.py b/tasks.py index 07fc64c03e..4522629e25 100644 --- a/tasks.py +++ b/tasks.py @@ -251,6 +251,7 @@ def content_excludes(): "contenttypes", "sessions.session", "auth.permission", + "authtoken.token", "error_report.error", "admin.logentry", "django_q.schedule", From 847d946fa44b240045586118931598b0e82ba911 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 20 Jun 2021 17:39:00 +1000 Subject: [PATCH 03/29] Bug fixes --- InvenTree/company/api.py | 2 +- InvenTree/templates/js/company.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/company/api.py b/InvenTree/company/api.py index 83aef7531b..3842045bac 100644 --- a/InvenTree/company/api.py +++ b/InvenTree/company/api.py @@ -249,7 +249,7 @@ class SupplierPartList(generics.ListCreateAPIView): params = self.request.query_params kwargs['part_detail'] = str2bool(params.get('part_detail', None)) kwargs['supplier_detail'] = str2bool(params.get('supplier_detail', None)) - kwargs['manufacturer_detail'] = str2bool(self.params.get('manufacturer_detail', None)) + kwargs['manufacturer_detail'] = str2bool(params.get('manufacturer_detail', None)) kwargs['pretty'] = str2bool(params.get('pretty', None)) except AttributeError: pass diff --git a/InvenTree/templates/js/company.js b/InvenTree/templates/js/company.js index be4b707466..c64a2fa062 100644 --- a/InvenTree/templates/js/company.js +++ b/InvenTree/templates/js/company.js @@ -260,7 +260,7 @@ function loadSupplierPartTable(table, url, options) { { sortable: true, field: 'supplier', - title: "{% trans "Supplier" %}", + title: '{% trans "Supplier" %}', formatter: function(value, row, index, field) { if (value) { var name = row.supplier_detail.name; @@ -276,7 +276,7 @@ function loadSupplierPartTable(table, url, options) { { sortable: true, field: 'SKU', - title: "{% trans "Supplier Part" %}", + title: '{% trans "Supplier Part" %}', formatter: function(value, row, index, field) { return renderLink(value, `/supplier-part/${row.pk}/`); } From 24733188288fcf8244f21660e8628566f549e1c2 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 20 Jun 2021 17:50:07 +1000 Subject: [PATCH 04/29] Add ManufacturerPartParameter model --- .../0038_manufacturerpartparameter.py | 27 ++++++++++++ InvenTree/company/models.py | 41 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 InvenTree/company/migrations/0038_manufacturerpartparameter.py diff --git a/InvenTree/company/migrations/0038_manufacturerpartparameter.py b/InvenTree/company/migrations/0038_manufacturerpartparameter.py new file mode 100644 index 0000000000..dccfa715e8 --- /dev/null +++ b/InvenTree/company/migrations/0038_manufacturerpartparameter.py @@ -0,0 +1,27 @@ +# Generated by Django 3.2.4 on 2021-06-20 07:48 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('company', '0037_supplierpart_update_3'), + ] + + operations = [ + migrations.CreateModel( + name='ManufacturerPartParameter', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='Parameter name', max_length=500, verbose_name='Name')), + ('value', models.CharField(help_text='Parameter value', max_length=500, verbose_name='Value')), + ('units', models.CharField(blank=True, help_text='Parameter units', max_length=64, null=True, verbose_name='Units')), + ('manufacturer_part', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='parameters', to='company.manufacturerpart', verbose_name='Manufacturer Part')), + ], + options={ + 'unique_together': {('manufacturer_part', 'name')}, + }, + ), + ] diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index a5fac00e7d..093d545f78 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -371,6 +371,47 @@ class ManufacturerPart(models.Model): return s +class ManufacturerPartParameter(models.Model): + """ + A ManufacturerPartParameter represents a key:value parameter for a MnaufacturerPart. + + This is used to represent parmeters / properties for a particular manufacturer part. + + Each parameter is a simple string (text) value. + """ + + class Meta: + unique_together = ('manufacturer_part', 'name') + + manufacturer_part = models.ForeignKey( + ManufacturerPart, + on_delete=models.CASCADE, + related_name='parameters', + verbose_name=_('Manufacturer Part'), + ) + + name = models.CharField( + max_length=500, + blank=False, + verbose_name=_('Name'), + help_text=_('Parameter name') + ) + + value = models.CharField( + max_length=500, + blank=False, + verbose_name=_('Value'), + help_text=_('Parameter value') + ) + + units = models.CharField( + max_length=64, + blank=True, null=True, + verbose_name=_('Units'), + help_text=_('Parameter units') + ) + + class SupplierPart(models.Model): """ Represents a unique part as provided by a Supplier Each SupplierPart is identified by a SKU (Supplier Part Number) From 310f3693940201dc88a31a691c5afb4b66f0a413 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 20 Jun 2021 23:49:44 +1000 Subject: [PATCH 05/29] Adds admin interface for ManufacturerPart --- InvenTree/company/admin.py | 40 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/InvenTree/company/admin.py b/InvenTree/company/admin.py index b339a65c0e..c5006dcfbf 100644 --- a/InvenTree/company/admin.py +++ b/InvenTree/company/admin.py @@ -11,6 +11,7 @@ import import_export.widgets as widgets from .models import Company from .models import SupplierPart from .models import SupplierPriceBreak +from .models import ManufacturerPart, ManufacturerPartParameter from part.models import Part @@ -71,6 +72,43 @@ class SupplierPartAdmin(ImportExportModelAdmin): ] +class ManufacturerPartResource(ModelResource): + """ + Class for managing ManufacturerPart data import/export + """ + + part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part)) + + part_name = Field(attribute='part__full_name', readonly=True) + + manufacturer = Field(attribute='manufacturer', widget=widgets.ForeignKeyWidget(Company)) + + manufacturer_name = Field(attribute='manufacturer__name', readonly=True) + + class Meta: + model = ManufacturerPart + skip_unchanged = True + report_skipped = True + clean_model_instances = True + + +class ManufacturerPartAdmin(ImportExportModelAdmin): + """ + Admin class for ManufacturerPart model + """ + + resource_class = ManufacturerPartResource + + list_display = ('part', 'manufacturer', 'MPN') + + search_fields = [ + 'manufacturer__name', + 'part__name', + 'MPN', + ] + + + class SupplierPriceBreakResource(ModelResource): """ Class for managing SupplierPriceBreak data import/export """ @@ -103,3 +141,5 @@ class SupplierPriceBreakAdmin(ImportExportModelAdmin): admin.site.register(Company, CompanyAdmin) admin.site.register(SupplierPart, SupplierPartAdmin) admin.site.register(SupplierPriceBreak, SupplierPriceBreakAdmin) + +admin.site.register(ManufacturerPart, ManufacturerPartAdmin) From 3fcb55275908d5cd104f504128ad345e43bc0a8b Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 20 Jun 2021 23:54:03 +1000 Subject: [PATCH 06/29] Register admin class for ManufacturerPartParameter --- InvenTree/company/admin.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/InvenTree/company/admin.py b/InvenTree/company/admin.py index c5006dcfbf..a32ca416a2 100644 --- a/InvenTree/company/admin.py +++ b/InvenTree/company/admin.py @@ -108,6 +108,33 @@ class ManufacturerPartAdmin(ImportExportModelAdmin): ] +class ManufacturerPartParameterResource(ModelResource): + """ + Class for managing ManufacturerPartParameter data import/export + """ + + class Meta: + model = ManufacturerPartParameter + skip_unchanged = True + report_skipped = True + clean_model_instance = True + + +class ManufacturerPartParameterAdmin(ImportExportModelAdmin): + """ + Admin class for ManufacturerPartParameter model + """ + + resource_class = ManufacturerPartParameterResource + + list_display = ('manufacturer_part', 'name', 'value') + + search_fields = [ + 'manufacturer_part__manufacturer__name', + 'name', + 'value' + ] + class SupplierPriceBreakResource(ModelResource): """ Class for managing SupplierPriceBreak data import/export """ @@ -143,3 +170,4 @@ admin.site.register(SupplierPart, SupplierPartAdmin) admin.site.register(SupplierPriceBreak, SupplierPriceBreakAdmin) admin.site.register(ManufacturerPart, ManufacturerPartAdmin) +admin.site.register(ManufacturerPartParameter, ManufacturerPartParameterAdmin) From 8188ba86a74b8385b2dc18850d4e07e10eb24e70 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 21 Jun 2021 00:00:55 +1000 Subject: [PATCH 07/29] Adds tabular inline class for ManufacturerPartParameter - Super cool! --- InvenTree/company/admin.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/InvenTree/company/admin.py b/InvenTree/company/admin.py index a32ca416a2..2ebd63acc9 100644 --- a/InvenTree/company/admin.py +++ b/InvenTree/company/admin.py @@ -92,6 +92,15 @@ class ManufacturerPartResource(ModelResource): clean_model_instances = True +class ManufacturerPartParameterInline(admin.TabularInline): + """ + Inline for editing ManufacturerPartParameter objects, + directly from the ManufacturerPart admin view. + """ + + model = ManufacturerPartParameter + + class ManufacturerPartAdmin(ImportExportModelAdmin): """ Admin class for ManufacturerPart model @@ -107,6 +116,10 @@ class ManufacturerPartAdmin(ImportExportModelAdmin): 'MPN', ] + inlines = [ + ManufacturerPartParameterInline + ] + class ManufacturerPartParameterResource(ModelResource): """ From 0013eb4c23468669d5c780ac9d2e2a6aea5e924e Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 21 Jun 2021 00:03:16 +1000 Subject: [PATCH 08/29] Adds inline for SupplierPart --- InvenTree/company/admin.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/InvenTree/company/admin.py b/InvenTree/company/admin.py index 2ebd63acc9..2c3be87c84 100644 --- a/InvenTree/company/admin.py +++ b/InvenTree/company/admin.py @@ -101,6 +101,14 @@ class ManufacturerPartParameterInline(admin.TabularInline): model = ManufacturerPartParameter +class SupplierPartInline(admin.TabularInline): + """ + Inline for the SupplierPart model + """ + + model = SupplierPart + + class ManufacturerPartAdmin(ImportExportModelAdmin): """ Admin class for ManufacturerPart model @@ -117,7 +125,8 @@ class ManufacturerPartAdmin(ImportExportModelAdmin): ] inlines = [ - ManufacturerPartParameterInline + SupplierPartInline, + ManufacturerPartParameterInline, ] From bea7ab7175b027d1eb49a28e24bf3cae00178fa0 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 21 Jun 2021 00:25:27 +1000 Subject: [PATCH 09/29] Add python version information to the "about" window --- InvenTree/part/templatetags/inventree_extras.py | 9 +++++++++ InvenTree/templates/about.html | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py index 90e91e167f..38689df26b 100644 --- a/InvenTree/part/templatetags/inventree_extras.py +++ b/InvenTree/part/templatetags/inventree_extras.py @@ -5,6 +5,7 @@ over and above the built-in Django tags. """ import os +import sys from django.utils.translation import ugettext_lazy as _ from django.conf import settings as djangosettings @@ -114,6 +115,14 @@ def inventree_title(*args, **kwargs): return version.inventreeInstanceTitle() +@register.simple_tag() +def python_version(*args, **kwargs): + """ + Return the current python version + """ + return sys.version.split(' ')[0] + + @register.simple_tag() def inventree_version(*args, **kwargs): """ Return InvenTree version string """ diff --git a/InvenTree/templates/about.html b/InvenTree/templates/about.html index 7b023295d1..358d468ca8 100644 --- a/InvenTree/templates/about.html +++ b/InvenTree/templates/about.html @@ -34,6 +34,11 @@ {% trans "API Version" %} {% inventree_api_version %}{% include "clip.html" %} + + + {% trans "Python Version" %} + {% python_version %} + {% trans "Django Version" %} From a00441a1c0ca791d07c72be763013d02fb2d1848 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 21 Jun 2021 00:28:28 +1000 Subject: [PATCH 10/29] Adds API endpoints for ManufacturerPartParameter mdoel --- InvenTree/InvenTree/version.py | 5 +- InvenTree/company/api.py | 91 +++++++++++++++++++++++++++++++- InvenTree/company/serializers.py | 31 ++++++++++- 3 files changed, 123 insertions(+), 4 deletions(-) diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py index 5161bee6a1..a47ef3b667 100644 --- a/InvenTree/InvenTree/version.py +++ b/InvenTree/InvenTree/version.py @@ -20,9 +20,12 @@ v4 -> 2021-06-01 - BOM items can now accept "variant stock" to be assigned against them - Many slight API tweaks were needed to get this to work properly! +v5 -> 2021-06-21 + - Adds API interface for manufacturer part parameters + """ -INVENTREE_API_VERSION = 4 +INVENTREE_API_VERSION = 5 def inventreeInstanceName(): diff --git a/InvenTree/company/api.py b/InvenTree/company/api.py index 3842045bac..6cd1e83dfa 100644 --- a/InvenTree/company/api.py +++ b/InvenTree/company/api.py @@ -15,11 +15,11 @@ from django.db.models import Q from InvenTree.helpers import str2bool from .models import Company -from .models import ManufacturerPart +from .models import ManufacturerPart, ManufacturerPartParameter from .models import SupplierPart, SupplierPriceBreak from .serializers import CompanySerializer -from .serializers import ManufacturerPartSerializer +from .serializers import ManufacturerPartSerializer, ManufacturerPartParameterSerializer from .serializers import SupplierPartSerializer, SupplierPriceBreakSerializer @@ -175,6 +175,86 @@ class ManufacturerPartDetail(generics.RetrieveUpdateDestroyAPIView): serializer_class = ManufacturerPartSerializer +class ManufacturerPartParameterList(generics.ListCreateAPIView): + """ + API endpoint for list view of ManufacturerPartParamater model. + """ + + queryset = ManufacturerPartParameter.objects.all() + serializer_class = ManufacturerPartParameterSerializer + + def get_serializer(self, *args, **kwargs): + + # Do we wish to include any extra detail? + try: + params = self.request.query_params + + optional_fields = [ + 'manufacturer_part_detail', + ] + + for key in optional_fields: + kwargs[key] = str2bool(params.get(key, None)) + + except AttributeError: + pass + + kwargs['context'] = self.get_serializer_context() + + return self.serializer_class(*args, **kwargs) + + def filter_queryset(self, queryset): + """ + Custom filtering for the queryset + """ + + queryset = super().filter_queryset(queryset) + + params = self.request.query_params + + # Filter by manufacturer? + manufacturer = params.get('manufacturer', None) + + if manufacturer is not None: + queryset = queryset.filter(manufacturer_part__manufacturer=manufacturer) + + # Filter by part? + part = params.get('part', None) + + if part is not None: + queryset = queryset.filter(manufacturer_part__part=part) + + return queryset + + filter_backends = [ + DjangoFilterBackend, + filters.SearchFilter, + filters.OrderingFilter, + ] + + filter_fields = [ + 'name', + 'value', + 'units', + 'manufacturer_part', + ] + + search_fields = [ + 'name', + 'value', + 'units', + ] + + +class ManufacturerPartParameterDetail(generics.RetrieveUpdateDestroyAPIView): + """ + API endpoint for detail view of ManufacturerPartParameter model + """ + + queryset = ManufacturerPartParameter.objects.all() + serializer_class = ManufacturerPartParameterSerializer + + class SupplierPartList(generics.ListCreateAPIView): """ API endpoint for list view of SupplierPart object @@ -316,6 +396,13 @@ class SupplierPriceBreakList(generics.ListCreateAPIView): manufacturer_part_api_urls = [ + url(r'^parameter/', include([ + url(r'^(?P\d+)/', ManufacturerPartParameterDetail.as_view(), name='api-manufacturer-part-parameter-detail'), + + # Catch anything else + url(r'^.*$', ManufacturerPartParameterList.as_view(), name='api-manufacturer-part-parameter-list'), + ])), + url(r'^(?P\d+)/?', ManufacturerPartDetail.as_view(), name='api-manufacturer-part-detail'), # Catch anything else diff --git a/InvenTree/company/serializers.py b/InvenTree/company/serializers.py index 4b1019656e..471d26bd3f 100644 --- a/InvenTree/company/serializers.py +++ b/InvenTree/company/serializers.py @@ -7,7 +7,7 @@ from rest_framework import serializers from sql_util.utils import SubqueryCount from .models import Company -from .models import ManufacturerPart +from .models import ManufacturerPart, ManufacturerPartParameter from .models import SupplierPart, SupplierPriceBreak from InvenTree.serializers import InvenTreeModelSerializer @@ -124,6 +124,35 @@ class ManufacturerPartSerializer(InvenTreeModelSerializer): ] +class ManufacturerPartParameterSerializer(InvenTreeModelSerializer): + """ + Serializer for the ManufacturerPartParameter model + """ + + manufacturer_part_detail = ManufacturerPartSerializer(source='manufacturer_part', many=False, read_only=True) + + def __init__(self, *args, **kwargs): + + man_detail = kwargs.pop('manufacturer_part_detail', False) + + super(ManufacturerPartParameterSerializer, self).__init__(*args, **kwargs) + + if not man_detail: + self.fields.pop('manufacturer_part_detail') + + class Meta: + model = ManufacturerPartParameter + + fields = [ + 'pk', + 'manufacturer_part', + 'manufacturer_part_detail', + 'name', + 'value', + 'units', + ] + + class SupplierPartSerializer(InvenTreeModelSerializer): """ Serializer for SupplierPart object """ From 70b6a3c13f5a092ca7c28daf6872bf922af960d7 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 21 Jun 2021 00:39:18 +1000 Subject: [PATCH 11/29] Display table of parameters --- .../company/manufacturer_part_suppliers.html | 36 +++++++++++- InvenTree/templates/js/company.js | 57 ++++++++++++++++++- InvenTree/templates/two_column.html | 6 ++ 3 files changed, 96 insertions(+), 3 deletions(-) diff --git a/InvenTree/company/templates/company/manufacturer_part_suppliers.html b/InvenTree/company/templates/company/manufacturer_part_suppliers.html index 647bf4750d..7a3f56a152 100644 --- a/InvenTree/company/templates/company/manufacturer_part_suppliers.html +++ b/InvenTree/company/templates/company/manufacturer_part_suppliers.html @@ -7,7 +7,7 @@ {% endblock %} {% block heading %} -{% trans "Supplier Parts" %} +{% trans "Suppliers" %} {% endblock %} {% block details %} @@ -30,6 +30,30 @@ {% endblock %} +{% block post_content_panels %} + +
+
+

{% trans "Parameters" %}

+
+
+
+
+ +
+ +
+
+
+ +
+
+
+ +{% endblock %} + {% block js_ready %} {{ block.super }} @@ -84,6 +108,16 @@ loadSupplierPartTable( } ); +loadManufacturerPartParameterTable( + "#parameter-table", + "{% url 'api-manufacturer-part-parameter-list' %}", + { + params: { + manufacturer_part: {{ part.id }}, + } + } +); + linkButtonsToSelection($("#supplier-table"), ['#supplier-part-options']) {% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/js/company.js b/InvenTree/templates/js/company.js index c64a2fa062..f0b0b4bd37 100644 --- a/InvenTree/templates/js/company.js +++ b/InvenTree/templates/js/company.js @@ -126,7 +126,7 @@ function loadManufacturerPartTable(table, url, options) { queryParams: filters, name: 'manufacturerparts', groupBy: false, - formatNoMatches: function() { return "{% trans "No manufacturer parts found" %}"; }, + formatNoMatches: function() { return '{% trans "No manufacturer parts found" %}'; }, columns: [ { checkbox: true, @@ -199,6 +199,59 @@ function loadManufacturerPartTable(table, url, options) { } +function loadManufacturerPartParameterTable(table, url, options) { + /* + * Load table of ManufacturerPartParameter objects + */ + + var params = options.params || {}; + + // Load filters + var filters = loadTableFilters("manufacturer-part-parameters"); + + // Overwrite explicit parameters + for (var key in params) { + filters[key] = params[key]; + } + + // setupFilterList("manufacturer-part-parameters", $(table)); + + $(table).inventreeTable({ + url: url, + method: 'get', + original: params, + queryParams: filters, + name: 'manufacturerpartparameters', + groupBy: false, + formatNoMatches: function() { return '{% trans "No parameters found" %}'; }, + columns: [ + { + checkbox: true, + switchable: false, + }, + { + field: 'name', + title: '{% trans "Name" %}', + switchable: false, + sortable: true, + }, + { + field: 'value', + title: '{% trans "Value" %}', + switchable: false, + sortable: true, + }, + { + field: 'units', + title: '{% trans "Units" %}', + switchable: true, + sortable: true, + } + ], + }); +} + + function loadSupplierPartTable(table, url, options) { /* * Load supplier part table @@ -224,7 +277,7 @@ function loadSupplierPartTable(table, url, options) { queryParams: filters, name: 'supplierparts', groupBy: false, - formatNoMatches: function() { return "{% trans "No supplier parts found" %}"; }, + formatNoMatches: function() { return '{% trans "No supplier parts found" %}'; }, columns: [ { checkbox: true, diff --git a/InvenTree/templates/two_column.html b/InvenTree/templates/two_column.html index 1eb1b5388e..c942bef1fa 100644 --- a/InvenTree/templates/two_column.html +++ b/InvenTree/templates/two_column.html @@ -43,6 +43,9 @@ {% endblock %} +{% block pre_content_panels %} +{% endblock %} + {% block content_panels %}
@@ -63,6 +66,9 @@
{% endblock %} +{% block post_content_panels %} +{% endblock %} + {% endblock %} {% block js_ready %} From 8a8b310195d1b04db0e5daebd5e9b6aea697c0bc Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 21 Jun 2021 00:46:15 +1000 Subject: [PATCH 12/29] Add some more inlines in the admin interface --- InvenTree/part/admin.py | 11 +++++++++++ InvenTree/stock/admin.py | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index 637dbbf2cf..2e434d928d 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -111,6 +111,13 @@ class PartCategoryResource(ModelResource): PartCategory.objects.rebuild() +class PartCategoryInline(admin.TabularInline): + """ + Inline for PartCategory model + """ + model = PartCategory + + class PartCategoryAdmin(ImportExportModelAdmin): resource_class = PartCategoryResource @@ -119,6 +126,10 @@ class PartCategoryAdmin(ImportExportModelAdmin): search_fields = ('name', 'description') + inlines = [ + PartCategoryInline, + ] + class PartRelatedAdmin(admin.ModelAdmin): ''' Class to manage PartRelated objects ''' diff --git a/InvenTree/stock/admin.py b/InvenTree/stock/admin.py index f32fa008a0..5f1b134966 100644 --- a/InvenTree/stock/admin.py +++ b/InvenTree/stock/admin.py @@ -44,6 +44,13 @@ class LocationResource(ModelResource): StockLocation.objects.rebuild() +class LocationInline(admin.TabularInline): + """ + Inline for sub-locations + """ + model = StockLocation + + class LocationAdmin(ImportExportModelAdmin): resource_class = LocationResource @@ -52,6 +59,10 @@ class LocationAdmin(ImportExportModelAdmin): search_fields = ('name', 'description') + inlines = [ + LocationInline, + ] + class StockItemResource(ModelResource): """ Class for managing StockItem data import/export """ From 4d0ce643a1560c2b8cd5199244e4c16248aae87c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Jun 2021 19:53:17 +0200 Subject: [PATCH 13/29] fix for timezone warning on heartbeat --- InvenTree/InvenTree/status.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/status.py b/InvenTree/InvenTree/status.py index 970e88831d..93b904d55b 100644 --- a/InvenTree/InvenTree/status.py +++ b/InvenTree/InvenTree/status.py @@ -4,9 +4,10 @@ Provides system status functionality checks. # -*- coding: utf-8 -*- from django.utils.translation import ugettext_lazy as _ +from django.utils import timezone import logging -from datetime import datetime, timedelta +from datetime import timedelta from django_q.models import Success from django_q.monitor import Stat @@ -34,7 +35,7 @@ def is_worker_running(**kwargs): Check to see if we have a result within the last 20 minutes """ - now = datetime.now() + now = timezone.now() past = now - timedelta(minutes=20) results = Success.objects.filter( From b32a6b50d29bcda2cbc8bbeaaa28ef854100138c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Jun 2021 20:16:14 +0200 Subject: [PATCH 14/29] fixes #1671 --- InvenTree/stock/templates/stock/item_base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index fa91177bf6..7aebd51452 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -325,7 +325,7 @@ {{ item.purchase_order }} {% endif %} - {% if item.purchase_price %} + {% if item.purchase_price != None %} {% trans "Purchase Price" %} From 59aae516528dcb1fa742616373ba2fc2abedc0e3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Jun 2021 20:17:01 +0200 Subject: [PATCH 15/29] show stock history starting by 1 element --- InvenTree/part/templates/part/order_prices.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/part/templates/part/order_prices.html b/InvenTree/part/templates/part/order_prices.html index a9da632a33..5e5552c5f9 100644 --- a/InvenTree/part/templates/part/order_prices.html +++ b/InvenTree/part/templates/part/order_prices.html @@ -137,7 +137,7 @@

{% trans 'Stock Pricing' %}

- {% if price_history|length > 1 %} + {% if price_history|length > 0 %}
From a0b83d530cb51738969617e7edc810c13ce137a8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Jun 2021 20:19:11 +0200 Subject: [PATCH 16/29] refactor --- InvenTree/part/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index b68c889516..39e0f13b45 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -814,7 +814,7 @@ class PartPricingView(PartDetail): part = self.get_part() # Stock history if part.total_stock > 1: - ret = [] + price_history = [] stock = part.stock_entries(include_variants=False, in_stock=True) # .order_by('purchase_order__date') stock = stock.prefetch_related('purchase_order', 'supplier_part') @@ -841,9 +841,9 @@ class PartPricingView(PartDetail): line['date'] = stock_item.purchase_order.issue_date.strftime('%d.%m.%Y') else: line['date'] = stock_item.tracking_info.first().date.strftime('%d.%m.%Y') - ret.append(line) + price_history.append(line) - ctx['price_history'] = ret + ctx['price_history'] = price_history # BOM Information for Pie-Chart if part.has_bom: From 80e47b6f766fdcd71c7496e7d5b12ddf95454290 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Jun 2021 22:49:06 +0200 Subject: [PATCH 17/29] addd in sorting for category fixes #1689 --- InvenTree/part/api.py | 1 + InvenTree/templates/js/part.js | 1 + 2 files changed, 2 insertions(+) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index c2785b666f..60cea121a7 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -706,6 +706,7 @@ class PartList(generics.ListCreateAPIView): 'creation_date', 'IPN', 'in_stock', + 'category', ] # Default ordering diff --git a/InvenTree/templates/js/part.js b/InvenTree/templates/js/part.js index 7331ec25e0..66174e2f15 100644 --- a/InvenTree/templates/js/part.js +++ b/InvenTree/templates/js/part.js @@ -447,6 +447,7 @@ function loadPartTable(table, url, options={}) { columns.push({ sortable: true, + sortName: 'category', field: 'category_detail', title: '{% trans "Category" %}', formatter: function(value, row, index, field) { From 213e4dccc54574d08afd270ac2986951decb1af7 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 21 Jun 2021 08:35:54 +1000 Subject: [PATCH 18/29] Add model to permission set --- InvenTree/users/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 0902b04ccd..09d70c3501 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -85,6 +85,7 @@ class RuleSet(models.Model): 'part_partstar', 'company_supplierpart', 'company_manufacturerpart', + 'company_manufacturerpartparameter', ], 'stock_location': [ 'stock_stocklocation', @@ -116,6 +117,8 @@ class RuleSet(models.Model): 'order_purchaseorderattachment', 'order_purchaseorderlineitem', 'company_supplierpart', + 'company_manufacturerpart', + 'company_manufacturerpartparameter', ], 'sales_order': [ 'company_company', From 8e9e04b4006d7f4781b00363b1ad1986f0dde11e Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 21 Jun 2021 10:23:53 +1000 Subject: [PATCH 19/29] Adds invoke task for deleting all database records --- tasks.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tasks.py b/tasks.py index 4522629e25..6ff998f586 100644 --- a/tasks.py +++ b/tasks.py @@ -365,6 +365,21 @@ def import_records(c, filename='data.json'): print("Data import completed") + +@task +def delete_data(c, force=False): + """ + Delete all database records! + + Warning: This will REALLY delete all records in the database!! + """ + + if force: + manage(c, 'flush', '--noinput') + else: + manage(c, 'flush') + + @task(post=[rebuild]) def import_fixtures(c): """ From 5198c5f9fa3000ed5924a7a160555acb06f79370 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 21 Jun 2021 10:38:50 +1000 Subject: [PATCH 20/29] Fix for delete-data command --- tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks.py b/tasks.py index 6ff998f586..b78a135b08 100644 --- a/tasks.py +++ b/tasks.py @@ -375,7 +375,7 @@ def delete_data(c, force=False): """ if force: - manage(c, 'flush', '--noinput') + manage(c, 'flush --noinput') else: manage(c, 'flush') From 32385f709b13978790315ebb36356d1cc825c16e Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 21 Jun 2021 10:52:40 +1000 Subject: [PATCH 21/29] Create RELEASE.md --- RELEASE.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 RELEASE.md diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000000..90fb027a62 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,23 @@ +## Release Checklist + +Checklist of steps to perform at each code release + +### Update Version String + +Update `INVENTREE_SW_VERSION` in [version.py](https://github.com/inventree/InvenTree/blob/master/InvenTree/InvenTree/version.py) + +### Increment API Version + +If the API has changed, ensure that the API version number is incremented. + +### Translation Files + +Merge the crowdin translation updates into master branch + +### Documentation Release + +Create new release for the [inventree documentation](https://github.com/inventree/inventree-docs) + +### Python Library Release + +Create new release for the [inventree python library](https://github.com/inventree/inventree-python) From 908039e1dbae7b06ddd528720a6c2ea4dc18a8dc Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 21 Jun 2021 11:10:29 +1000 Subject: [PATCH 22/29] Perform unique checks for InvenTreeModelSerializer in DRF - Prevents ValidationError from throwing in the wrong spot and not being handled --- InvenTree/InvenTree/serializers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index fa7674723c..eea8072bf3 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -50,6 +50,7 @@ class InvenTreeModelSerializer(serializers.ModelSerializer): # Now ensure the underlying model is correct instance = self.Meta.model(**data) + instance.validate_unique() instance.clean() return data From 04b216253d795559cb5889bffad95cd536f581e0 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 21 Jun 2021 11:30:20 +1000 Subject: [PATCH 23/29] Ok, that was a bad idea. --- InvenTree/InvenTree/serializers.py | 1 - InvenTree/part/models.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index eea8072bf3..fa7674723c 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -50,7 +50,6 @@ class InvenTreeModelSerializer(serializers.ModelSerializer): # Now ensure the underlying model is correct instance = self.Meta.model(**data) - instance.validate_unique() instance.clean() return data diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 6c05d62a7e..8aa370a7ae 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -380,7 +380,6 @@ class Part(MPTTModel): previous.image.delete(save=False) self.clean() - self.validate_unique() super().save(*args, **kwargs) @@ -672,6 +671,8 @@ class Part(MPTTModel): super().clean() + self.validate_unique() + if self.trackable: for part in self.get_used_in().all(): From e0e560352ea983f740b5052a7093da3fc528da24 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 21 Jun 2021 16:14:58 +1000 Subject: [PATCH 24/29] Add forms / views for ManufacturerPartParameter CRUD --- InvenTree/company/forms.py | 17 ++++- .../company/manufacturer_part_suppliers.html | 11 ++++ InvenTree/company/urls.py | 25 +++++--- InvenTree/company/views.py | 64 ++++++++++++++++++- InvenTree/templates/js/company.js | 48 ++++++++++++++ 5 files changed, 152 insertions(+), 13 deletions(-) diff --git a/InvenTree/company/forms.py b/InvenTree/company/forms.py index 6ffa94b746..80673b4fa4 100644 --- a/InvenTree/company/forms.py +++ b/InvenTree/company/forms.py @@ -16,7 +16,7 @@ from djmoney.forms.fields import MoneyField from common.settings import currency_code_default -from .models import Company +from .models import Company, ManufacturerPartParameter from .models import ManufacturerPart from .models import SupplierPart from .models import SupplierPriceBreak @@ -105,6 +105,21 @@ class EditManufacturerPartForm(HelperForm): ] +class EditManufacturerPartParameterForm(HelperForm): + """ + Form for creating / editing a ManufacturerPartParameter object + """ + + class Meta: + model = ManufacturerPartParameter + fields = [ + 'manufacturer_part', + 'name', + 'value', + 'units', + ] + + class EditSupplierPartForm(HelperForm): """ Form for editing a SupplierPart object """ diff --git a/InvenTree/company/templates/company/manufacturer_part_suppliers.html b/InvenTree/company/templates/company/manufacturer_part_suppliers.html index 7a3f56a152..9f445ec215 100644 --- a/InvenTree/company/templates/company/manufacturer_part_suppliers.html +++ b/InvenTree/company/templates/company/manufacturer_part_suppliers.html @@ -57,6 +57,17 @@ {% block js_ready %} {{ block.super }} +$('#parameter-create').click(function() { + launchModalForm( + "{% url 'manufacturer-part-parameter-create' %}", + { + data: { + manufacturer_part: {{ part.id }}, + } + } + ); +}); + $('#supplier-create').click(function () { launchModalForm( "{% url 'supplier-part-create' %}", diff --git a/InvenTree/company/urls.py b/InvenTree/company/urls.py index 11f1be5339..51aa81f1c7 100644 --- a/InvenTree/company/urls.py +++ b/InvenTree/company/urls.py @@ -53,20 +53,25 @@ price_break_urls = [ url(r'^(?P\d+)/delete/', views.PriceBreakDelete.as_view(), name='price-break-delete'), ] -manufacturer_part_detail_urls = [ - url(r'^edit/?', views.ManufacturerPartEdit.as_view(), name='manufacturer-part-edit'), - - url(r'^suppliers/', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part_suppliers.html'), name='manufacturer-part-suppliers'), - - url('^.*$', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part_suppliers.html'), name='manufacturer-part-detail'), -] - manufacturer_part_urls = [ url(r'^new/?', views.ManufacturerPartCreate.as_view(), name='manufacturer-part-create'), - url(r'delete/', views.ManufacturerPartDelete.as_view(), name='manufacturer-part-delete'), + url(r'^delete/', views.ManufacturerPartDelete.as_view(), name='manufacturer-part-delete'), - url(r'^(?P\d+)/', include(manufacturer_part_detail_urls)), + # URLs for ManufacturerPartParameter views (create / edit / delete) + url(r'^parameter/', include([ + url(r'^new/', views.ManufacturerPartParameterCreate.as_view(), name='manufacturer-part-parameter-create'), + url(r'^(?P\d)/', include([ + url(r'^edit/', views.ManufacturerPartParameterEdit.as_view(), name='manufacturer-part-parameter-edit'), + url(r'^delete/', views.ManufacturerPartParameterDelete.as_view(), name='manufacturer-part-parameter-delete'), + ])), + ])), + + url(r'^(?P\d+)/', include([ + url(r'^edit/?', views.ManufacturerPartEdit.as_view(), name='manufacturer-part-edit'), + url(r'^suppliers/', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part_suppliers.html'), name='manufacturer-part-suppliers'), + url('^.*$', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part_suppliers.html'), name='manufacturer-part-detail'), + ])), ] supplier_part_detail_urls = [ diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py index 6de1439823..b2f655737f 100644 --- a/InvenTree/company/views.py +++ b/InvenTree/company/views.py @@ -23,14 +23,14 @@ from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView from InvenTree.helpers import str2bool from InvenTree.views import InvenTreeRoleMixin -from .models import Company +from .models import Company, ManufacturerPartParameter from .models import ManufacturerPart from .models import SupplierPart from .models import SupplierPriceBreak from part.models import Part -from .forms import EditCompanyForm +from .forms import EditCompanyForm, EditManufacturerPartParameterForm from .forms import CompanyImageForm from .forms import EditManufacturerPartForm from .forms import EditSupplierPartForm @@ -504,6 +504,66 @@ class ManufacturerPartDelete(AjaxDeleteView): return self.renderJsonResponse(self.request, data=data, form=self.get_form()) +class ManufacturerPartParameterCreate(AjaxCreateView): + """ + View for creating a new ManufacturerPartParameter object + """ + + model = ManufacturerPartParameter + form_class = EditManufacturerPartParameterForm + ajax_form_title = _('Add Manufacturer Part Parameter') + + def get_form(self): + + form = super().get_form() + + # Hide the manufacturer_part field if specified + if form.initial.get('manufacturer_part', None): + form.fields['manufacturer_part'].widget = HiddenInput() + + return form + + def get_initial(self): + + initials = super().get_initial().copy() + + manufacturer_part = self.get_param('manufacturer_part') + + if manufacturer_part: + try: + initials['manufacturer_part'] = ManufacturerPartParameter.objects.get(pk=manufacturer_part) + except (ValueError, ManufacturerPartParameter.DoesNotExist): + pass + + return initials + + +class ManufacturerPartParameterEdit(AjaxUpdateView): + """ + View for editing a ManufacturerPartParameter object + """ + + model = ManufacturerPartParameter + form_class = EditManufacturerPartParameterForm + ajax_form_title = _('Edit Manufacturer Part Parameter') + + def get_form(self): + + form = super().get_form() + + form.fields['manufacturer_part'].widget = HiddenInput() + + return form + + +class ManufacturerPartParameterDelete(AjaxDeleteView): + """ + View for deleting a ManufacturerPartParameter object + """ + + model = ManufacturerPartParameter + + class SupplierPartDetail(DetailView): """ Detail view for SupplierPart """ model = SupplierPart diff --git a/InvenTree/templates/js/company.js b/InvenTree/templates/js/company.js index f0b0b4bd37..078b40f4b9 100644 --- a/InvenTree/templates/js/company.js +++ b/InvenTree/templates/js/company.js @@ -228,6 +228,7 @@ function loadManufacturerPartParameterTable(table, url, options) { { checkbox: true, switchable: false, + visible: false, }, { field: 'name', @@ -246,8 +247,55 @@ function loadManufacturerPartParameterTable(table, url, options) { title: '{% trans "Units" %}', switchable: true, sortable: true, + }, + { + field: 'actions', + title: '', + switchable: false, + sortable: false, + formatter: function(value, row) { + + var pk = row.pk; + + var html = `
`; + + html += makeIconButton('fa-edit icon-blue', 'button-parameter-edit', pk, '{% trans "Edit parameter" %}'); + html += makeIconButton('fa-trash-alt icon-red', 'button-parameter-delete', pk, '{% trans "Delete parameter" %}'); + + html += `
`; + + return html; + } } ], + onPostBody: function() { + // Setup callback functions + $(table).find('.button-parameter-edit').click(function() { + var pk = $(this).attr('pk'); + + launchModalForm( + `/manufacturer-part/parameter/${pk}/edit/`, + { + success: function() { + $(table).bootstrapTable('refresh'); + } + } + ); + + }); + $(table).find('.button-parameter-delete').click(function() { + var pk = $(this).attr('pk'); + + launchModalForm( + `/manufacturer-part/parameter/${pk}/delete/`, + { + success: function() { + $(table).bootstrapTable('refresh'); + } + } + ); + }); + } }); } From e6598b51156b3376a7851179fb6c78bfb9d27069 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 21 Jun 2021 16:58:39 +1000 Subject: [PATCH 25/29] Displays "purchase order" column in StockItem table --- .../order/templates/order/order_base.html | 2 +- .../templates/order/sales_order_base.html | 2 +- InvenTree/stock/serializers.py | 8 +++++++ InvenTree/templates/js/stock.js | 21 +++++++++++++++++++ 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/InvenTree/order/templates/order/order_base.html b/InvenTree/order/templates/order/order_base.html index 642f866506..c7ba6be8a4 100644 --- a/InvenTree/order/templates/order/order_base.html +++ b/InvenTree/order/templates/order/order_base.html @@ -75,7 +75,7 @@ src="{% static 'img/blank_image.png' %}" {% trans "Order Reference" %} - {{ order.reference }}{% include "clip.html"%} + {% settings_value 'PURCHASEORDER_REFERENCE_PREFIX' %}{{ order.reference }}{% include "clip.html"%} diff --git a/InvenTree/order/templates/order/sales_order_base.html b/InvenTree/order/templates/order/sales_order_base.html index d3f4ba74a2..b342cefe66 100644 --- a/InvenTree/order/templates/order/sales_order_base.html +++ b/InvenTree/order/templates/order/sales_order_base.html @@ -77,7 +77,7 @@ src="{% static 'img/blank_image.png' %}" {% trans "Order Reference" %} - {{ order.reference }}{% include "clip.html"%} + {% settings_value 'SALESORDER_REFERENCE_PREFIX' %}{{ order.reference }}{% include "clip.html"%} diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index d60689ccef..a0b7e3403a 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -81,6 +81,7 @@ class StockItemSerializer(InvenTreeModelSerializer): 'belongs_to', 'build', 'customer', + 'purchase_order', 'sales_order', 'supplier_part', 'supplier_part__supplier', @@ -163,6 +164,10 @@ class StockItemSerializer(InvenTreeModelSerializer): purchase_price = serializers.SerializerMethodField() + purchase_order_reference = serializers.CharField(source='purchase_order.reference', read_only=True) + + sales_order_reference = serializers.CharField(source='sales_order.reference', read_only=True) + def get_purchase_price(self, obj): """ Return purchase_price (Money field) as string (includes currency) """ @@ -208,10 +213,13 @@ class StockItemSerializer(InvenTreeModelSerializer): 'packaging', 'part', 'part_detail', + 'purchase_order', + 'purchase_order_reference', 'pk', 'quantity', 'required_tests', 'sales_order', + 'sales_order_reference', 'serial', 'stale', 'status', diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js index 4e992df67f..4dd2093896 100644 --- a/InvenTree/templates/js/stock.js +++ b/InvenTree/templates/js/stock.js @@ -660,6 +660,27 @@ function loadStockTable(table, options) { title: '{% trans "Last Updated" %}', sortable: true, }, + { + field: 'purchase_order', + title: '{% trans "Purchase Order" %}', + formatter: function(value, row) { + if (!value) { + return '-'; + } + + var link = `/order/purchase-order/${row.purchase_order}/`; + var text = `${row.purchase_order}`; + + if (row.purchase_order_reference) { + + var prefix = '{% settings_value "PURCHASEORDER_REFERENCE_PREFIX" %}'; + + text = prefix + row.purchase_order_reference; + } + + return renderLink(text, link); + } + }, { field: 'purchase_price', title: '{% trans "Purchase Price" %}', From 6d294183645a653c934bb54b188e7012d66eec41 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 21 Jun 2021 17:03:00 +1000 Subject: [PATCH 26/29] Prevent "rebuild" command from running certain things --- InvenTree/InvenTree/ready.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/InvenTree/ready.py b/InvenTree/InvenTree/ready.py index 9e64a2e6c7..4acbcae9af 100644 --- a/InvenTree/InvenTree/ready.py +++ b/InvenTree/InvenTree/ready.py @@ -33,6 +33,7 @@ def canAppAccessDatabase(): 'createsuperuser', 'wait_for_db', 'prerender', + 'rebuild', 'collectstatic', 'makemessages', 'compilemessages', From ff33cc43bd66a1931370e603162aed9ebf13e06b Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 21 Jun 2021 17:28:41 +1000 Subject: [PATCH 27/29] Create initial migration file --- .../migrations/0064_auto_20210621_1724.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 InvenTree/stock/migrations/0064_auto_20210621_1724.py diff --git a/InvenTree/stock/migrations/0064_auto_20210621_1724.py b/InvenTree/stock/migrations/0064_auto_20210621_1724.py new file mode 100644 index 0000000000..bc40a7523b --- /dev/null +++ b/InvenTree/stock/migrations/0064_auto_20210621_1724.py @@ -0,0 +1,41 @@ +# Generated by Django 3.2.4 on 2021-06-21 07:24 + +from django.db import migrations + + +def extract_purchase_price(apps, schema_editor): + """ + Find instances of StockItem which do *not* have a purchase price set, + but which point to a PurchaseOrder where there *is* a purchase price set. + + Then, assign *that* purchase price to original StockItem. + + This is to address an issue where older versions of InvenTree + did not correctly copy purchase price information cross to the StockItem objects. + + Current InvenTree version (as of 2021-06-21) copy this information across correctly, + so this one-time data migration should suffice. + """ + + # Required database models + StockItem = apps.get_model('stock', 'stockitem') + PurchaseOrder = apps.get_model('order', 'purchaseorder') + Part = apps.get_model('part', 'part') + + +def reverse_operation(apps, schema_editor): + """ + DO NOTHING! + """ + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0063_auto_20210511_2343'), + ] + + operations = [ + migrations.RunPython(extract_purchase_price, reverse_code=reverse_operation) + ] From 02540edd589c51cdd20bb330174fb45cc58d7cc9 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 21 Jun 2021 20:30:15 +1000 Subject: [PATCH 28/29] Copy purchase price information across in the data migration --- .../migrations/0064_auto_20210621_1724.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/InvenTree/stock/migrations/0064_auto_20210621_1724.py b/InvenTree/stock/migrations/0064_auto_20210621_1724.py index bc40a7523b..71314f366c 100644 --- a/InvenTree/stock/migrations/0064_auto_20210621_1724.py +++ b/InvenTree/stock/migrations/0064_auto_20210621_1724.py @@ -20,8 +20,43 @@ def extract_purchase_price(apps, schema_editor): # Required database models StockItem = apps.get_model('stock', 'stockitem') PurchaseOrder = apps.get_model('order', 'purchaseorder') + PurchaseOrderLineItem = apps.get_model('order', 'purchaseorderlineitem') Part = apps.get_model('part', 'part') + # Find all the StockItem objects without a purchase_price which point to a PurchaseOrder + items = StockItem.objects.filter(purchase_price=None).exclude(purchase_order=None) + + print(f"Found {items.count()} stock items with missing purchase price information") + + update_count = 0 + + for item in items: + + part_id = item.part + + po = item.purchase_order + + # Look for a matching PurchaseOrderLineItem (with a price) + lines = PurchaseOrderLineItem.objects.filter(part__part=part_id, order=po) + + if lines.exists(): + + for line in lines: + if line.purchase_price is not None: + + # Copy pricing information across + item.purchase_price = line.purchase_price + item.purchases_price_currency = line.purchase_price_currency + + print(f"- Updating supplier price for {item.part.name} - {item.purchase_price} {item.purchase_price_currency}") + + update_count += 1 + + item.save() + + break + + print(f"Updated pricing for {update_count} stock items") def reverse_operation(apps, schema_editor): """ From d1a2ed9af17801aceec415fa56d343ff527ff29d Mon Sep 17 00:00:00 2001 From: Adam Walsh Date: Mon, 21 Jun 2021 11:20:43 -0400 Subject: [PATCH 29/29] Fix supplier part view/edit without a manufacturer Fixes the following issues when a supplier part was created with an MPN but no manufacturer was assigned: - Viewing a supplier part stock item - Editing a supplier part stock item --- InvenTree/company/views.py | 3 ++- InvenTree/stock/templates/stock/item_base.html | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py index b2f655737f..74a583710a 100644 --- a/InvenTree/company/views.py +++ b/InvenTree/company/views.py @@ -623,7 +623,8 @@ class SupplierPartEdit(AjaxUpdateView): supplier_part = self.get_object() if supplier_part.manufacturer_part: - initials['manufacturer'] = supplier_part.manufacturer_part.manufacturer.id + if supplier_part.manufacturer_part.manufacturer: + initials['manufacturer'] = supplier_part.manufacturer_part.manufacturer.id initials['MPN'] = supplier_part.manufacturer_part.MPN return initials diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index 7aebd51452..bf9d10590f 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -350,7 +350,12 @@ {% trans "Manufacturer" %} - {{ item.supplier_part.manufacturer_part.manufacturer.name }} + {% if item.supplier_part.manufacturer_part.manufacturer %} + {{ item.supplier_part.manufacturer_part.manufacturer.name }} + {% else %} + {% trans "No manufacturer set" %} + {% endif %} +