From b4f8136142aa0901ed9da32bfe9dd28c8193f3b8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 21 Mar 2022 21:58:21 +1100 Subject: [PATCH 01/85] Don't add "remove row" button if there is only one row --- InvenTree/templates/js/translated/order.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index 9d66fa2ab4..ed0ddbb4fa 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -655,12 +655,14 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) { ); } - buttons += makeIconButton( - 'fa-times icon-red', - 'button-row-remove', - pk, - '{% trans "Remove row" %}', - ); + if (line_items.length > 1) { + buttons += makeIconButton( + 'fa-times icon-red', + 'button-row-remove', + pk, + '{% trans "Remove row" %}', + ); + } buttons += ''; From 64bbcd25707eecb83a3f94a25253973836de7ec6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 21 Mar 2022 22:41:50 +1100 Subject: [PATCH 02/85] Add validation checks for the PurchaseOrderLineItem serializer --- InvenTree/order/serializers.py | 37 +++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 2f4c1ea5df..56e21b1b2b 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -164,8 +164,23 @@ class POLineItemSerializer(InvenTreeModelSerializer): if order_detail is not True: self.fields.pop('order_detail') - quantity = serializers.FloatField(default=1) - received = serializers.FloatField(default=0) + quantity = serializers.FloatField(min_value=0, required=True) + + def validate_quantity(self, quantity): + + if quantity <= 0: + raise ValidationError(_("Quantity must be greater than zero")) + + return quantity + + def validate_purchase_order(self, purchase_order): + + if purchase_order.status not in PurchaseOrderStatus.OPEN: + raise ValidationError(_('Order is not open')) + + return purchase_order + + received = serializers.FloatField(default=0, read_only=True) overdue = serializers.BooleanField(required=False, read_only=True) @@ -189,6 +204,22 @@ class POLineItemSerializer(InvenTreeModelSerializer): order_detail = POSerializer(source='order', read_only=True, many=False) + def validate(self, data): + + data = super().validate(data) + + supplier_part = data['part'] + purchase_order = data['order'] + + # Check that the supplier part and purchase order match + if supplier_part is not None and supplier_part.supplier != purchase_order.supplier: + raise ValidationError({ + 'part': _('Supplier must match purchase order'), + 'order': _('Purchase order must match supplier'), + }) + + return data + class Meta: model = order.models.PurchaseOrderLineItem @@ -349,7 +380,7 @@ class POReceiveSerializer(serializers.Serializer): Serializer for receiving items against a purchase order """ - items = POLineItemReceiveSerializer(many=True) + items = POLineItemReceiveSerializer(many=True, required=True) location = serializers.PrimaryKeyRelatedField( queryset=stock.models.StockLocation.objects.all(), From f2806b2e41e8219359f1f6aea6aeab8e10005a81 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 21 Mar 2022 23:19:27 +1100 Subject: [PATCH 03/85] Tweaks for existing form code --- InvenTree/templates/js/translated/forms.js | 5 +++++ InvenTree/templates/js/translated/model_renderers.js | 10 ++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/InvenTree/templates/js/translated/forms.js b/InvenTree/templates/js/translated/forms.js index 88c9c5badb..e01835ae36 100644 --- a/InvenTree/templates/js/translated/forms.js +++ b/InvenTree/templates/js/translated/forms.js @@ -1780,6 +1780,11 @@ function initializeRelatedField(field, fields, options={}) { // Only a single result is available, given the provided filters if (data.count == 1) { setRelatedFieldData(name, data.results[0], options); + + // Run "callback" function (if supplied) + if (field.onEdit) { + field.onEdit(data.results[0], name, field, options); + } } } }); diff --git a/InvenTree/templates/js/translated/model_renderers.js b/InvenTree/templates/js/translated/model_renderers.js index e3abe1186f..72c1ed378b 100644 --- a/InvenTree/templates/js/translated/model_renderers.js +++ b/InvenTree/templates/js/translated/model_renderers.js @@ -371,10 +371,16 @@ function renderSupplierPart(name, data, parameters, options) { var html = ''; html += select2Thumbnail(supplier_image); - html += select2Thumbnail(part_image); + + if (data.part_detail) { + html += select2Thumbnail(part_image); + } html += ` ${data.supplier_detail.name} - ${data.SKU}`; - html += ` - ${data.part_detail.full_name}`; + + if (data.part_detail) { + html += ` - ${data.part_detail.full_name}`; + } html += `{% trans "Supplier Part ID" %}: ${data.pk}`; From 4fc605ee2857943859e2b411366648ed1b23ed68 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 23 Mar 2022 21:26:11 +1100 Subject: [PATCH 04/85] Render a form for ordering parts --- InvenTree/part/templates/part/part_base.html | 16 ++ InvenTree/templates/js/translated/order.js | 212 +++++++++++++++++++ 2 files changed, 228 insertions(+) diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index d14cfbdfd5..e47396d714 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -558,6 +558,22 @@ {% endif %} $("#part-order").click(function() { + + inventreeGet( + '{% url "api-part-detail" part.pk %}', + {}, + { + success: function(part) { + orderParts( + [part], + {} + ); + } + } + ); + + return; + launchModalForm("{% url 'order-parts' %}", { data: { part: {{ part.id }}, diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index ed0ddbb4fa..78f6d39ee9 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -33,6 +33,7 @@ loadSalesOrderTable, newPurchaseOrderFromOrderWizard, newSupplierPartFromOrderWizard, + orderParts, removeOrderRowFromOrderWizard, removePurchaseOrderLineItem, */ @@ -450,6 +451,217 @@ function exportOrder(redirect_url, options={}) { }); } + +/* + * Create a new form to order parts based on the list of provided parts. + */ +function orderParts(parts_list, options={}) { + + var parts = []; + + parts_list.forEach(function(part) { + if (part.purchaseable) { + parts.push(part); + } + }); + + if (parts.length == 0) { + showAlertDialog( + '{% trans "Select Parts" %}', + '{% trans "At least one purchaseable part must be selected" %}', + ); + return; + } + + // Render a single part within the dialog + function renderPart(part, opts={}) { + + var pk = part.pk; + + var thumb = thumbnailImage(part.thumbnail || part.image); + + // The "quantity" field should have been provided for each part + var quantity = part.quantity || 0; + + if (quantity < 0) { + quantity = 0; + } + + var quantity_input = constructField( + `items_quantity_${pk}`, + { + type: 'decimal', + min_value: 0, + value: quantity, + title: '{% trans "Quantity to order" %}', + required: true, + }, + { + hideLabels: true, + } + ) + + var supplier_part_prefix = ` + + + + `; + + var supplier_part_input = constructField( + `supplier_part_${pk}`, + { + type: 'related field', + required: true, + prefixRaw: supplier_part_prefix, + }, + { + hideLabels: true, + } + ); + + var purchase_order_prefix = ` + + + + `; + + var purchase_order_input = constructField( + `purchase_order_${pk}`, + { + type: 'related field', + required: true, + prefixRaw: purchase_order_prefix, + }, + { + hideLabels: 'true', + } + ); + + var buttons = `
`; + + buttons += makeIconButton( + 'fa-check-circle icon-green', + 'button-row-complete', + pk, + '{% trans "Add to order" %}', + ); + + if (parts.length > 1) { + buttons += makeIconButton( + 'fa-times icon-red', + 'button-row-remove', + pk, + '{% trans "Remove row" %}', + ); + } + + buttons += `
`; + + var html = ` + + ${thumb} ${part.full_name} + ${supplier_part_input} + ${purchase_order_input} + ${quantity_input} + ${buttons} + + `; + + return html; + } + + var table_entries = ''; + + parts.forEach(function(part) { + table_entries += renderPart(part); + }); + + var html = ''; + + // Add table + html += ` + + + + + + + + + + + + ${table_entries} + +
{% trans "Part" %}{% trans "Supplier Part" %}{% trans "Purchase Order" %}{% trans "Quantity" %}
+ `; + + constructFormBody({}, { + preFormContent: html, + title: '{% trans "Order Parts" %}', + hideSubmitButton: true, + afterRender: function(fields, opts) { + // TODO + parts.forEach(function(part) { + // Configure the "supplier part" field + initializeRelatedField({ + name: `supplier_part_${part.pk}`, + model: 'supplierpart', + api_url: '{% url "api-supplier-part-list" %}', + required: true, + type: 'related field', + auto_fill: true, + filters: { + part: part.pk, + supplier_detail: true, + part_detail: false, + }, + noResults: function(query) { + return '{% trans "No matching supplier parts" %}'; + } + }, null, opts); + + // Configure the "purchase order" field + initializeRelatedField({ + name: `purchase_order_${part.pk}`, + model: 'purchaseorder', + api_url: '{% url "api-po-list" %}', + required: true, + type: 'related field', + auto_fill: false, + filters: { + status: {{ PurchaseOrderStatus.PENDING }}, + }, + adjustFilters: function(query, opts) { + + // Whenever we open the drop-down to select an order, + // ensure we are only displaying orders which match the selected supplier part + var supplier_part_pk = getFormFieldValue(`supplier_part_${part.pk}`, opts); + + inventreeGet( + `/api/company/part/${supplier_part_pk}/`, + {}, + { + async: false, + success: function(data) { + query.supplier = data.supplier; + } + } + ); + + return query; + }, + noResults: function(query) { + return '{% trans "No matching purchase orders" %}'; + } + }, null, opts); + + }); + } + }); + +} + function newPurchaseOrderFromOrderWizard(e) { /* Create a new purchase order directly from an order form. * Launches a secondary modal and (if successful), From aa30e62ad80b120d3b36bf2024cd844a060263a9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Mar 2022 01:29:36 +0200 Subject: [PATCH 05/85] [BUG] Deleting a Customer Breaks Associated Sales Orders Add special protected deleted company Fixes #2788 --- .../migrations/0043_company_is_deleted.py | 18 +++++++++++++ InvenTree/company/models.py | 16 +++++++++++- .../migrations/0064_auto_20220329_2246.py | 25 +++++++++++++++++++ InvenTree/order/models.py | 7 ++++-- 4 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 InvenTree/company/migrations/0043_company_is_deleted.py create mode 100644 InvenTree/order/migrations/0064_auto_20220329_2246.py diff --git a/InvenTree/company/migrations/0043_company_is_deleted.py b/InvenTree/company/migrations/0043_company_is_deleted.py new file mode 100644 index 0000000000..cf42f7cc6a --- /dev/null +++ b/InvenTree/company/migrations/0043_company_is_deleted.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.12 on 2022-03-29 22:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('company', '0042_supplierpricebreak_updated'), + ] + + operations = [ + migrations.AddField( + model_name='company', + name='is_deleted', + field=models.BooleanField(default=False, help_text='Is this company a deleted placeholder?', verbose_name='is deleted'), + ), + ] diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index f72668f9f0..8058b7e358 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -9,7 +9,7 @@ import os from django.utils.translation import ugettext_lazy as _ from django.core.validators import MinValueValidator -from django.core.exceptions import ValidationError +from django.core.exceptions import ValidationError, PermissionDenied from django.db import models from django.db.models import Sum, Q, UniqueConstraint @@ -147,6 +147,8 @@ class Company(models.Model): is_manufacturer = models.BooleanField(default=False, verbose_name=_('is manufacturer'), help_text=_('Does this company manufacture parts?')) + is_deleted = models.BooleanField(default=False, verbose_name=_('is deleted'), help_text=_('Is this company a deleted placeholder?')) + currency = models.CharField( max_length=3, verbose_name=_('Currency'), @@ -266,6 +268,18 @@ class Company(models.Model): return self.purchase_orders.filter(status__in=PurchaseOrderStatus.FAILED) + def save(self, *args, **kwargs): + """Save the instance, unless it is the magic already deleted object""" + if self.pk and self.is_deleted: + raise PermissionDenied(_('This company is a placeholder and can not be updated')) + return super().save(*args, **kwargs) + + def delete(self, *args, **kwargs): + """Delete the instance, unless it is the magic already deleted object""" + if self.is_deleted: + raise PermissionDenied(_('This company is a placeholder and can not be deleted')) + return super().delete(*args, **kwargs) + class Contact(models.Model): """ A Contact represents a person who works at a particular company. diff --git a/InvenTree/order/migrations/0064_auto_20220329_2246.py b/InvenTree/order/migrations/0064_auto_20220329_2246.py new file mode 100644 index 0000000000..ad0a804d11 --- /dev/null +++ b/InvenTree/order/migrations/0064_auto_20220329_2246.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.12 on 2022-03-29 22:46 + +from django.db import migrations, models +import order.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('company', '0043_company_is_deleted'), + ('order', '0063_alter_purchaseorderlineitem_unique_together'), + ] + + operations = [ + migrations.AlterField( + model_name='purchaseorder', + name='supplier', + field=models.ForeignKey(help_text='Company from which the items are being ordered', limit_choices_to={'is_supplier': True}, on_delete=models.SET(order.models.get_deleted_company), related_name='purchase_orders', to='company.company', verbose_name='Supplier'), + ), + migrations.AlterField( + model_name='salesorder', + name='customer', + field=models.ForeignKey(help_text='Company to which the items are being sold', limit_choices_to={'is_customer': True}, null=True, on_delete=models.SET(order.models.get_deleted_company), related_name='sales_orders', to='company.company', verbose_name='Customer'), + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index f08880a882..9225f3faf8 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -92,6 +92,9 @@ def get_next_so_number(): return reference +def get_deleted_company(): + return Company.objects.get_or_create(name='deleted', email='deleted',is_deleted=True)[0] + class Order(ReferenceIndexingMixin): """ Abstract model for an order. @@ -219,7 +222,7 @@ class PurchaseOrder(Order): help_text=_('Purchase order status')) supplier = models.ForeignKey( - Company, on_delete=models.CASCADE, + Company, on_delete=models.SET(get_deleted_company), limit_choices_to={ 'is_supplier': True, }, @@ -567,7 +570,7 @@ class SalesOrder(Order): customer = models.ForeignKey( Company, - on_delete=models.SET_NULL, + on_delete=models.SET(get_deleted_company), null=True, limit_choices_to={'is_customer': True}, related_name='sales_orders', From 6c6c47c60e29f7ff113427e01f5b7d3e57203280 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Mar 2022 01:30:02 +0200 Subject: [PATCH 06/85] do not import / export --- InvenTree/company/admin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/company/admin.py b/InvenTree/company/admin.py index 97327a559a..5a19d2c391 100644 --- a/InvenTree/company/admin.py +++ b/InvenTree/company/admin.py @@ -21,6 +21,7 @@ class CompanyResource(ModelResource): class Meta: model = Company + exclude = ('is_deleted', ) skip_unchanged = True report_skipped = False clean_model_instances = True From d068b0a064cd522506646975e3b441c33b56fbb4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Mar 2022 01:30:21 +0200 Subject: [PATCH 07/85] do not show in admin --- InvenTree/company/admin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/InvenTree/company/admin.py b/InvenTree/company/admin.py index 5a19d2c391..af78e76a7f 100644 --- a/InvenTree/company/admin.py +++ b/InvenTree/company/admin.py @@ -38,6 +38,8 @@ class CompanyAdmin(ImportExportModelAdmin): 'description', ] + exclude = ('is_deleted',) + class SupplierPartResource(ModelResource): """ From 44239cba081fd3afbce3401fed6721898e25c15d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Mar 2022 01:30:32 +0200 Subject: [PATCH 08/85] hide delete button --- InvenTree/company/admin.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/InvenTree/company/admin.py b/InvenTree/company/admin.py index af78e76a7f..6412aff934 100644 --- a/InvenTree/company/admin.py +++ b/InvenTree/company/admin.py @@ -40,6 +40,11 @@ class CompanyAdmin(ImportExportModelAdmin): exclude = ('is_deleted',) + def has_delete_permission(self, request, obj=None): + if obj and obj.is_deleted: + return False + return True + class SupplierPartResource(ModelResource): """ From 0d2fbe16f5d2ae583478767fb236231059a3a4c4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Mar 2022 01:31:02 +0200 Subject: [PATCH 09/85] hide the action buttons if special object --- InvenTree/company/templates/company/company_base.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/company/templates/company/company_base.html b/InvenTree/company/templates/company/company_base.html index 489493fd06..4caf37272e 100644 --- a/InvenTree/company/templates/company/company_base.html +++ b/InvenTree/company/templates/company/company_base.html @@ -27,12 +27,12 @@ +{% endif %} {% endblock actions %} {% block thumbnail %} From 924e46a0e357abb5444732abf352e580447676ae Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Mar 2022 01:32:09 +0200 Subject: [PATCH 11/85] hide purchase buttopn if special button --- InvenTree/company/templates/company/company_base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/company/templates/company/company_base.html b/InvenTree/company/templates/company/company_base.html index 6bf4c929b3..4adf4adb3b 100644 --- a/InvenTree/company/templates/company/company_base.html +++ b/InvenTree/company/templates/company/company_base.html @@ -18,7 +18,7 @@ {% url 'admin:company_company_change' company.pk as url %} {% include "admin_button.html" with url=url %} {% endif %} -{% if company.is_supplier and roles.purchase_order.add %} +{% if company.is_supplier and roles.purchase_order.add and not company.is_deleted %} From 48441ea48e51fe2a61625ce9146b61f6b00cdc36 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Mar 2022 01:32:29 +0200 Subject: [PATCH 12/85] add docstring --- InvenTree/order/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 9225f3faf8..a9db03ca54 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -93,6 +93,9 @@ def get_next_so_number(): def get_deleted_company(): + """ + Returns the deleted company object + """ return Company.objects.get_or_create(name='deleted', email='deleted',is_deleted=True)[0] class Order(ReferenceIndexingMixin): From e7f940810ad926b8f1fd82c078f1255d4827d259 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Mar 2022 01:32:41 +0200 Subject: [PATCH 13/85] PEP style fix --- InvenTree/order/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index a9db03ca54..270e1ca223 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -98,6 +98,7 @@ def get_deleted_company(): """ return Company.objects.get_or_create(name='deleted', email='deleted',is_deleted=True)[0] + class Order(ReferenceIndexingMixin): """ Abstract model for an order. From 1d24f3586d3b95bcdc7d9947acd07d6d019c6ddb Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Mar 2022 01:35:21 +0200 Subject: [PATCH 14/85] PEP fix --- InvenTree/order/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 270e1ca223..ee18fb9a21 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -96,7 +96,7 @@ def get_deleted_company(): """ Returns the deleted company object """ - return Company.objects.get_or_create(name='deleted', email='deleted',is_deleted=True)[0] + return Company.objects.get_or_create(name='deleted', email='deleted', is_deleted=True)[0] class Order(ReferenceIndexingMixin): From c278a5397f1fa65fb37842ec9d66a57a5138bda7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 30 Mar 2022 01:42:00 +0200 Subject: [PATCH 15/85] add doc --- InvenTree/company/admin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/company/admin.py b/InvenTree/company/admin.py index 6412aff934..af767f3fdc 100644 --- a/InvenTree/company/admin.py +++ b/InvenTree/company/admin.py @@ -41,6 +41,7 @@ class CompanyAdmin(ImportExportModelAdmin): exclude = ('is_deleted',) def has_delete_permission(self, request, obj=None): + """Magic objects are not allowd to be deleted""" if obj and obj.is_deleted: return False return True From 57f9ef75e9bb2cf36ccaa9ab291691527ea3acef Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 1 Apr 2022 23:55:08 +0200 Subject: [PATCH 16/85] enable all functions for deleted company --- InvenTree/order/models.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index ee18fb9a21..9dcb360bcd 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -96,7 +96,14 @@ def get_deleted_company(): """ Returns the deleted company object """ - return Company.objects.get_or_create(name='deleted', email='deleted', is_deleted=True)[0] + return Company.objects.get_or_create( + name='deleted', + email='deleted', + is_deleted=True, + is_customer = True, + is_supplier = True, + is_manufacturer = True + )[0] class Order(ReferenceIndexingMixin): From 80fa8f6d1849e175cc9aece62f85bc66af3fc708 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 1 Apr 2022 23:57:49 +0200 Subject: [PATCH 17/85] remove double definition --- InvenTree/company/models.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 8058b7e358..7bb3a0701d 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -304,9 +304,6 @@ class Contact(models.Model): role = models.CharField(max_length=100, blank=True) - company = models.ForeignKey(Company, related_name='contacts', - on_delete=models.CASCADE) - class ManufacturerPart(models.Model): """ Represents a unique part as provided by a Manufacturer From 66e14b6ad02051f7e5dda868d97695f3dcc56aa1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Apr 2022 00:01:38 +0200 Subject: [PATCH 18/85] move helper function to models.py --- InvenTree/company/models.py | 14 ++++++++++++++ InvenTree/order/models.py | 16 +--------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 7bb3a0701d..8d010f0457 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -60,6 +60,20 @@ def rename_company_image(instance, filename): return os.path.join(base, fn) +def get_deleted_company(): + """ + Returns the deleted company object + """ + return Company.objects.get_or_create( + name='deleted', + email='deleted', + is_deleted=True, + is_customer = True, + is_supplier = True, + is_manufacturer = True + )[0] + + class Company(models.Model): """ A Company object represents an external company. It may be a supplier or a customer or a manufacturer (or a combination) diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 9dcb360bcd..b9bec62890 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -24,7 +24,7 @@ from mptt.models import TreeForeignKey from users import models as UserModels from part import models as PartModels from stock import models as stock_models -from company.models import Company, SupplierPart +from company.models import Company, SupplierPart, get_deleted_company from plugin.events import trigger_event import InvenTree.helpers @@ -92,20 +92,6 @@ def get_next_so_number(): return reference -def get_deleted_company(): - """ - Returns the deleted company object - """ - return Company.objects.get_or_create( - name='deleted', - email='deleted', - is_deleted=True, - is_customer = True, - is_supplier = True, - is_manufacturer = True - )[0] - - class Order(ReferenceIndexingMixin): """ Abstract model for an order. From 07aaa457de573c0d85b9735416dce44c58ec8401 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Apr 2022 00:04:25 +0200 Subject: [PATCH 19/85] PEP fix --- InvenTree/company/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 8d010f0457..7866fbc8a7 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -68,9 +68,9 @@ def get_deleted_company(): name='deleted', email='deleted', is_deleted=True, - is_customer = True, - is_supplier = True, - is_manufacturer = True + is_customer=True, + is_supplier=True, + is_manufacturer=True )[0] From e1fbd961e50706a4e884392b0101caf4352eb428 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 7 Apr 2022 19:05:25 +1000 Subject: [PATCH 20/85] Refactor form renderer functions --- .../js/translated/model_renderers.js | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/InvenTree/templates/js/translated/model_renderers.js b/InvenTree/templates/js/translated/model_renderers.js index e45c4298a9..bdc2528af9 100644 --- a/InvenTree/templates/js/translated/model_renderers.js +++ b/InvenTree/templates/js/translated/model_renderers.js @@ -34,8 +34,8 @@ // Should the ID be rendered for this string function renderId(title, pk, parameters={}) { - // Default = true - var render = true; + // Default = do not render + var render = false; if ('render_pk' in parameters) { render = parameters['render_pk']; @@ -138,7 +138,7 @@ function renderStockLocation(name, data, parameters={}, options={}) { html += ` - ${data.description}`; } - html += `{% trans "Location ID" %}: ${data.pk}`; + html += renderId('{% trans "Location ID" %}', data.pk, parameters); return html; } @@ -155,7 +155,8 @@ function renderBuild(name, data, parameters={}, options={}) { var html = select2Thumbnail(image); html += `${data.reference} - ${data.quantity} x ${data.part_detail.full_name}`; - html += `{% trans "Build ID" %}: ${data.pk}`; + + html += renderId('{% trans "Build ID" %}', data.pk, parameters); html += `

${data.title}

`; @@ -293,12 +294,13 @@ function renderSalesOrderShipment(name, data, parameters={}, options={}) { var so_prefix = global_settings.SALESORDER_REFERENCE_PREFIX; var html = ` - ${so_prefix}${data.order_detail.reference} - {% trans "Shipment" %} ${data.reference} - - {% trans "Shipment ID" %}: ${data.pk} + + ${so_prefix}${data.order_detail.reference} - {% trans "Shipment" %} ${data.reference} `; + html += renderId('{% trans "Shipment ID" %}', data.pk, parameters); + return html; } @@ -315,7 +317,7 @@ function renderPartCategory(name, data, parameters={}, options={}) { html += ` - ${data.description}`; } - html += `{% trans "Category ID" %}: ${data.pk}`; + html += renderId('{% trans "Category ID" %}', data.pk, parameters); return html; } @@ -358,7 +360,7 @@ function renderManufacturerPart(name, data, parameters={}, options={}) { html += ` ${data.manufacturer_detail.name} - ${data.MPN}`; html += ` - ${data.part_detail.full_name}`; - html += `{% trans "Manufacturer Part ID" %}: ${data.pk}`; + html += renderId('{% trans "Manufacturer Part ID" %}', data.pk, parameters); return html; } @@ -393,8 +395,7 @@ function renderSupplierPart(name, data, parameters={}, options={}) { html += ` - ${data.part_detail.full_name}`; } - html += `{% trans "Supplier Part ID" %}: ${data.pk}`; - + html += renderId('{% trans "Supplier Part ID" %}', data.pk, parameters); return html; From e225d3b7657ea23d447fb222353819bf1b136d48 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 7 Apr 2022 19:09:43 +1000 Subject: [PATCH 21/85] Fix action buttons in "part" table on category page --- InvenTree/part/templates/part/category.html | 13 +++++++++---- InvenTree/templates/js/translated/order.js | 2 +- InvenTree/templates/js/translated/part.js | 13 ------------- 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/InvenTree/part/templates/part/category.html b/InvenTree/part/templates/part/category.html index 6a61ef2fbf..8c15b6151f 100644 --- a/InvenTree/part/templates/part/category.html +++ b/InvenTree/part/templates/part/category.html @@ -169,13 +169,18 @@ {% include "filter_list.html" with id="parts" %} diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index 78f6d39ee9..6b1db9fc4c 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -481,7 +481,7 @@ function orderParts(parts_list, options={}) { var thumb = thumbnailImage(part.thumbnail || part.image); // The "quantity" field should have been provided for each part - var quantity = part.quantity || 0; + var quantity = part.quantity || 1; if (quantity < 0) { quantity = 0; diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js index 08b258fdc2..f26d87653a 100644 --- a/InvenTree/templates/js/translated/part.js +++ b/InvenTree/templates/js/translated/part.js @@ -1581,19 +1581,6 @@ function loadPartTable(table, url, options={}) { printPartLabels(items); }); - - $('#multi-part-export').click(function() { - var selections = $(table).bootstrapTable('getSelections'); - - var parts = ''; - - selections.forEach(function(item) { - parts += item.pk; - parts += ','; - }); - - location.href = '/part/export/?parts=' + parts; - }); } From c66cd1d51b448e22297c1693c8b689549069d3ae Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 26 Apr 2022 22:14:01 +1000 Subject: [PATCH 22/85] Adds button to expand row for "extra" information --- InvenTree/templates/js/translated/order.js | 29 ++++++++++++++++++---- InvenTree/templates/js/translated/part.js | 15 +++++------ 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index 6b1db9fc4c..f55deeacef 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -540,10 +540,13 @@ function orderParts(parts_list, options={}) { var buttons = `
`; buttons += makeIconButton( - 'fa-check-circle icon-green', - 'button-row-complete', + 'fa-layer-group', + 'button-row-expand', pk, - '{% trans "Add to order" %}', + '{% trans "Expand Row" %}', + { + collapseTarget: `order_row_expand_${pk}`, + } ); if (parts.length > 1) { @@ -564,8 +567,18 @@ function orderParts(parts_list, options={}) { ${purchase_order_input} ${quantity_input} ${buttons} - - `; + `; + + // Add a second row "underneath" the first one, but collapsed + // Allows extra data to be added if required, but hidden by default + html += ` + + + reference goes here + + + + `; return html; } @@ -656,6 +669,12 @@ function orderParts(parts_list, options={}) { } }, null, opts); + // Add callback for "remove row" button + $(opts.modal).find('.button-row-remove').click(function() { + var pk = $(this).attr('pk'); + + $(opts.modal).find(`#order_row_${pk}`).remove(); + }); }); } }); diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js index 6fd144d5f0..3aa3b936cf 100644 --- a/InvenTree/templates/js/translated/part.js +++ b/InvenTree/templates/js/translated/part.js @@ -1559,15 +1559,16 @@ function loadPartTable(table, url, options={}) { var parts = []; - selections.forEach(function(item) { - parts.push(item.pk); + selections.forEach(function(part) { + parts.push(part); }); - launchModalForm('/order/purchase-order/order-parts/', { - data: { - parts: parts, - }, - }); + orderParts( + parts, + { + + } + ); }); $('#multi-part-category').click(function() { From bf11e8361e80c4de1f94e5b67470ef310aa17242 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 26 Apr 2022 22:18:27 +1000 Subject: [PATCH 23/85] Add (empty) callbacks to prefix buttons --- InvenTree/templates/js/translated/order.js | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index f55deeacef..8db8f68067 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -502,9 +502,9 @@ function orderParts(parts_list, options={}) { ) var supplier_part_prefix = ` - + `; var supplier_part_input = constructField( @@ -520,9 +520,9 @@ function orderParts(parts_list, options={}) { ); var purchase_order_prefix = ` - + `; var purchase_order_input = constructField( @@ -675,6 +675,20 @@ function orderParts(parts_list, options={}) { $(opts.modal).find(`#order_row_${pk}`).remove(); }); + + // Add callback for "new supplier part" button + $(opts.modal).find('.button-row-new-sp').click(function() { + var pk = $(this).attr('pk'); + + // Launch dialog to create new supplier part + }); + + // Add callback for "new purchase order" button + $(opts.modal).find('.button-row-new-po').click(function() { + var pk = $(this).attr('pk'); + + // Launch dialog to create new purchase order + }); }); } }); From 340d4d8a897656469d2ca266f61cd9571eb10954 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 27 Apr 2022 00:12:12 +1000 Subject: [PATCH 24/85] Launch new forms --- InvenTree/templates/js/translated/order.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index 8db8f68067..723b462adb 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -681,6 +681,12 @@ function orderParts(parts_list, options={}) { var pk = $(this).attr('pk'); // Launch dialog to create new supplier part + createSupplierPart({ + part: pk, + onSuccess: function(response) { + // TODO + } + }); }); // Add callback for "new purchase order" button @@ -688,6 +694,11 @@ function orderParts(parts_list, options={}) { var pk = $(this).attr('pk'); // Launch dialog to create new purchase order + createPurchaseOrder({ + onSuccess: function(response) { + // TODO + } + }); }); }); } From 24af2bd2c8c23e6ae47ab2b2b480e7682a4169b5 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 27 Apr 2022 21:45:00 +1000 Subject: [PATCH 25/85] Update console output for forms.js --- InvenTree/templates/js/translated/forms.js | 50 +++++++++++----------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/InvenTree/templates/js/translated/forms.js b/InvenTree/templates/js/translated/forms.js index e01835ae36..01f9e162eb 100644 --- a/InvenTree/templates/js/translated/forms.js +++ b/InvenTree/templates/js/translated/forms.js @@ -135,7 +135,7 @@ function getApiEndpointOptions(url, callback) { success: callback, error: function(xhr) { // TODO: Handle error - console.log(`ERROR in getApiEndpointOptions at '${url}'`); + console.error(`Error in getApiEndpointOptions at '${url}'`); showApiError(xhr, url); } }); @@ -227,7 +227,7 @@ function constructChangeForm(fields, options) { }, error: function(xhr) { // TODO: Handle error here - console.log(`ERROR in constructChangeForm at '${options.url}'`); + console.error(`Error in constructChangeForm at '${options.url}'`); showApiError(xhr, options.url); } @@ -268,7 +268,7 @@ function constructDeleteForm(fields, options) { }, error: function(xhr) { // TODO: Handle error here - console.log(`ERROR in constructDeleteForm at '${options.url}`); + console.error(`Error in constructDeleteForm at '${options.url}`); showApiError(xhr, options.url); } @@ -354,7 +354,7 @@ function constructForm(url, options) { icon: 'fas fa-user-times', }); - console.log(`'POST action unavailable at ${url}`); + console.warn(`'POST action unavailable at ${url}`); } break; case 'PUT': @@ -369,7 +369,7 @@ function constructForm(url, options) { icon: 'fas fa-user-times', }); - console.log(`${options.method} action unavailable at ${url}`); + console.warn(`${options.method} action unavailable at ${url}`); } break; case 'DELETE': @@ -383,7 +383,7 @@ function constructForm(url, options) { icon: 'fas fa-user-times', }); - console.log(`DELETE action unavailable at ${url}`); + console.warn(`DELETE action unavailable at ${url}`); } break; case 'GET': @@ -397,11 +397,11 @@ function constructForm(url, options) { icon: 'fas fa-user-times', }); - console.log(`GET action unavailable at ${url}`); + console.warn(`GET action unavailable at ${url}`); } break; default: - console.log(`constructForm() called with invalid method '${options.method}'`); + console.warn(`constructForm() called with invalid method '${options.method}'`); break; } }); @@ -731,7 +731,7 @@ function submitFormData(fields, options) { data[name] = value; } } else { - console.log(`WARNING: Could not find field matching '${name}'`); + console.warn(`Could not find field matching '${name}'`); } } @@ -776,7 +776,7 @@ function submitFormData(fields, options) { default: $(options.modal).modal('hide'); - console.log(`upload error at ${options.url}`); + console.error(`Upload error at ${options.url}`); showApiError(xhr, options.url); break; } @@ -827,7 +827,7 @@ function updateFieldValue(name, value, field, options) { var el = getFormFieldElement(name, options); if (!el) { - console.log(`WARNING: updateFieldValue could not find field '${name}'`); + console.warn(`updateFieldValue could not find field '${name}'`); return; } @@ -870,7 +870,7 @@ function getFormFieldElement(name, options) { } if (!el.exists) { - console.log(`ERROR: Could not find form element for field '${name}'`); + console.error(`Could not find form element for field '${name}'`); } return el; @@ -918,7 +918,7 @@ function getFormFieldValue(name, field={}, options={}) { var el = getFormFieldElement(name, options); if (!el.exists()) { - console.log(`ERROR: getFormFieldValue could not locate field '${name}'`); + console.error(`getFormFieldValue could not locate field '${name}'`); return null; } @@ -1104,7 +1104,7 @@ function handleNestedErrors(errors, field_name, options={}) { // Nest list must be provided! if (!nest_list) { - console.log(`WARNING: handleNestedErrors missing nesting options for field '${fieldName}'`); + console.warn(`handleNestedErrors missing nesting options for field '${fieldName}'`); return; } @@ -1113,7 +1113,7 @@ function handleNestedErrors(errors, field_name, options={}) { var error_item = error_list[idx]; if (idx >= nest_list.length) { - console.log(`WARNING: handleNestedErrors returned greater number of errors (${error_list.length}) than could be handled (${nest_list.length})`); + console.warn(`handleNestedErrors returned greater number of errors (${error_list.length}) than could be handled (${nest_list.length})`); break; } @@ -1285,7 +1285,7 @@ function addFieldErrorMessage(name, error_text, error_idx=0, options={}) { field_dom.append(error_html); } else { - console.log(`WARNING: addFieldErrorMessage could not locate field '${field_name}'`); + console.warn(`addFieldErrorMessage could not locate field '${field_name}'`); } } @@ -1358,7 +1358,7 @@ function addClearCallback(name, field, options={}) { } if (!el) { - console.log(`WARNING: addClearCallback could not find field '${name}'`); + console.warn(`addClearCallback could not find field '${name}'`); return; } @@ -1582,7 +1582,7 @@ function initializeRelatedField(field, fields, options={}) { var name = field.name; if (!field.api_url) { - console.log(`WARNING: Related field '${name}' missing 'api_url' parameter.`); + console.warn(`Related field '${name}' missing 'api_url' parameter.`); return; } @@ -1712,7 +1712,7 @@ function initializeRelatedField(field, fields, options={}) { return $(html); } else { // Return a simple renderering - console.log(`WARNING: templateResult() missing 'field.model' for '${name}'`); + console.warn(`templateResult() missing 'field.model' for '${name}'`); return `${name} - ${item.id}`; } }, @@ -1742,7 +1742,7 @@ function initializeRelatedField(field, fields, options={}) { return $(html); } else { // Return a simple renderering - console.log(`WARNING: templateSelection() missing 'field.model' for '${name}'`); + console.warn(`templateSelection() missing 'field.model' for '${name}'`); return `${name} - ${item.id}`; } } @@ -1916,7 +1916,7 @@ function renderModelData(name, model, data, parameters, options) { if (html != null) { return html; } else { - console.log(`ERROR: Rendering not implemented for model '${model}'`); + console.error(`Rendering not implemented for model '${model}'`); // Simple text rendering return `${model} - ID ${data.id}`; } @@ -2201,7 +2201,7 @@ function constructInput(name, parameters, options={}) { if (func != null) { html = func(name, parameters, options); } else { - console.log(`WARNING: Unhandled form field type: '${parameters.type}'`); + console.warn(`Unhandled form field type: '${parameters.type}'`); } return html; @@ -2504,12 +2504,12 @@ function constructHelpText(name, parameters) { function selectImportFields(url, data={}, options={}) { if (!data.model_fields) { - console.log(`WARNING: selectImportFields is missing 'model_fields'`); + console.warn(`selectImportFields is missing 'model_fields'`); return; } if (!data.file_fields) { - console.log(`WARNING: selectImportFields is missing 'file_fields'`); + console.warn(`selectImportFields is missing 'file_fields'`); return; } @@ -2600,7 +2600,7 @@ function selectImportFields(url, data={}, options={}) { default: $(opts.modal).modal('hide'); - console.log(`upload error at ${opts.url}`); + console.error(`upload error at ${opts.url}`); showApiError(xhr, opts.url); break; } From 132f4aa82e427c2befdf7b6ebcaf2959bb03ef45 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 1 May 2022 00:07:13 +0200 Subject: [PATCH 26/85] Use set_null instead --- InvenTree/company/admin.py | 9 --------- InvenTree/company/models.py | 28 ---------------------------- InvenTree/order/models.py | 6 +++--- 3 files changed, 3 insertions(+), 40 deletions(-) diff --git a/InvenTree/company/admin.py b/InvenTree/company/admin.py index af767f3fdc..97327a559a 100644 --- a/InvenTree/company/admin.py +++ b/InvenTree/company/admin.py @@ -21,7 +21,6 @@ class CompanyResource(ModelResource): class Meta: model = Company - exclude = ('is_deleted', ) skip_unchanged = True report_skipped = False clean_model_instances = True @@ -38,14 +37,6 @@ class CompanyAdmin(ImportExportModelAdmin): 'description', ] - exclude = ('is_deleted',) - - def has_delete_permission(self, request, obj=None): - """Magic objects are not allowd to be deleted""" - if obj and obj.is_deleted: - return False - return True - class SupplierPartResource(ModelResource): """ diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 7866fbc8a7..e4b3a1b640 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -60,20 +60,6 @@ def rename_company_image(instance, filename): return os.path.join(base, fn) -def get_deleted_company(): - """ - Returns the deleted company object - """ - return Company.objects.get_or_create( - name='deleted', - email='deleted', - is_deleted=True, - is_customer=True, - is_supplier=True, - is_manufacturer=True - )[0] - - class Company(models.Model): """ A Company object represents an external company. It may be a supplier or a customer or a manufacturer (or a combination) @@ -161,8 +147,6 @@ class Company(models.Model): is_manufacturer = models.BooleanField(default=False, verbose_name=_('is manufacturer'), help_text=_('Does this company manufacture parts?')) - is_deleted = models.BooleanField(default=False, verbose_name=_('is deleted'), help_text=_('Is this company a deleted placeholder?')) - currency = models.CharField( max_length=3, verbose_name=_('Currency'), @@ -282,18 +266,6 @@ class Company(models.Model): return self.purchase_orders.filter(status__in=PurchaseOrderStatus.FAILED) - def save(self, *args, **kwargs): - """Save the instance, unless it is the magic already deleted object""" - if self.pk and self.is_deleted: - raise PermissionDenied(_('This company is a placeholder and can not be updated')) - return super().save(*args, **kwargs) - - def delete(self, *args, **kwargs): - """Delete the instance, unless it is the magic already deleted object""" - if self.is_deleted: - raise PermissionDenied(_('This company is a placeholder and can not be deleted')) - return super().delete(*args, **kwargs) - class Contact(models.Model): """ A Contact represents a person who works at a particular company. diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index b9bec62890..2eb5b0d69c 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -24,7 +24,7 @@ from mptt.models import TreeForeignKey from users import models as UserModels from part import models as PartModels from stock import models as stock_models -from company.models import Company, SupplierPart, get_deleted_company +from company.models import Company, SupplierPart from plugin.events import trigger_event import InvenTree.helpers @@ -219,7 +219,7 @@ class PurchaseOrder(Order): help_text=_('Purchase order status')) supplier = models.ForeignKey( - Company, on_delete=models.SET(get_deleted_company), + Company, on_delete=models.SET_NULL, limit_choices_to={ 'is_supplier': True, }, @@ -567,7 +567,7 @@ class SalesOrder(Order): customer = models.ForeignKey( Company, - on_delete=models.SET(get_deleted_company), + on_delete=models.SET_NULL, null=True, limit_choices_to={'is_customer': True}, related_name='sales_orders', From 9947cc2b08721e2727bf7a95ce1b65ee0b7983e1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 1 May 2022 00:07:37 +0200 Subject: [PATCH 27/85] remove old permissionset --- InvenTree/company/templates/company/company_base.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/InvenTree/company/templates/company/company_base.html b/InvenTree/company/templates/company/company_base.html index 4adf4adb3b..c58ea63791 100644 --- a/InvenTree/company/templates/company/company_base.html +++ b/InvenTree/company/templates/company/company_base.html @@ -18,23 +18,23 @@ {% url 'admin:company_company_change' company.pk as url %} {% include "admin_button.html" with url=url %} {% endif %} -{% if company.is_supplier and roles.purchase_order.add and not company.is_deleted %} +{% if company.is_supplier and roles.purchase_order.add %} {% endif %} {% define perms.company.change_company or perms.company.delete_company as has_permission %} -{% if not company.is_deleted and has_permission %} +{% if has_permission %}
+ {% endif %} {% endfor %} diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html index 23bdd908e1..542a7a90ca 100644 --- a/InvenTree/order/templates/order/purchase_order_detail.html +++ b/InvenTree/order/templates/order/purchase_order_detail.html @@ -170,10 +170,12 @@ $('#new-po-line').click(function() { var fields = poLineItemFields({ order: {{ order.pk }}, + {% if order.supplier %} supplier: {{ order.supplier.pk }}, {% if order.supplier.currency %} currency: '{{ order.supplier.currency }}', {% endif %} + {% endif %} }); constructForm('{% url "api-po-line-list" %}', { @@ -210,7 +212,9 @@ $('#new-po-line').click(function() { loadPurchaseOrderLineItemTable('#po-line-table', { order: {{ order.pk }}, + {% if order.supplier %} supplier: {{ order.supplier.pk }}, + {% endif %} {% if roles.purchase_order.change %} allow_edit: true, {% else %} diff --git a/InvenTree/part/templates/part/partial_delete.html b/InvenTree/part/templates/part/partial_delete.html index eb23fbee09..22c739b833 100644 --- a/InvenTree/part/templates/part/partial_delete.html +++ b/InvenTree/part/templates/part/partial_delete.html @@ -43,7 +43,7 @@

{% blocktrans with count=part.manufacturer_parts.all|length %}There are {{count}} manufacturers defined for this part. If you delete this part, the following manufacturer parts will also be deleted:{% endblocktrans %}

    {% for spart in part.manufacturer_parts.all %} -
  • {{ spart.manufacturer.name }} - {{ spart.MPN }}
  • +
  • {% if spart.manufacturer %}{{ spart.manufacturer.name }} - {% endif %}{{ spart.MPN }}
  • {% endfor %}

@@ -54,7 +54,9 @@

{% blocktrans with count=part.supplier_parts.all|length %}There are {{count}} suppliers defined for this part. If you delete this part, the following supplier parts will also be deleted:{% endblocktrans %}

    {% for spart in part.supplier_parts.all %} + {% if spart.supplier %}
  • {{ spart.supplier.name }} - {{ spart.SKU }}
  • + {% endif %} {% endfor %}

diff --git a/InvenTree/report/templates/report/inventree_po_report.html b/InvenTree/report/templates/report/inventree_po_report.html index d1cae75c3b..427410576c 100644 --- a/InvenTree/report/templates/report/inventree_po_report.html +++ b/InvenTree/report/templates/report/inventree_po_report.html @@ -74,7 +74,7 @@ table td.expand {

{% trans "Purchase Order" %} {{ prefix }}{{ reference }}

- {{ supplier.name }} + {% if supplier %}{{ supplier.name }}{% endif %}
{% endblock %} diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index dd77d26d1c..4c8af402cf 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -379,7 +379,11 @@ {% trans "Supplier" %} - {{ item.supplier_part.supplier.name }} + + {% if item.supplier_part.supplier %} + {{ item.supplier_part.supplier.name }} + {% endif %} + From 87515b1b48a11c61d41cede8d2ad2fc49b626dc3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 2 May 2022 22:46:03 +0200 Subject: [PATCH 35/85] fix __str__ instance --- InvenTree/order/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 65aa368798..060d638de1 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -228,7 +228,7 @@ class PurchaseOrder(Order): prefix = getSetting('PURCHASEORDER_REFERENCE_PREFIX') - return f"{prefix}{self.reference} - {self.supplier.name}" + return f"{prefix}{self.reference} - {self.supplier.name if self.supplier else _('deleted')}" reference = models.CharField( unique=True, @@ -576,7 +576,7 @@ class SalesOrder(Order): prefix = getSetting('SALESORDER_REFERENCE_PREFIX') - return f"{prefix}{self.reference} - {self.customer.name}" + return f"{prefix}{self.reference} - {self.customer.name if self.customer else _('deleted')}" def get_absolute_url(self): return reverse('so-detail', kwargs={'pk': self.id}) @@ -939,7 +939,7 @@ class PurchaseOrderLineItem(OrderLineItem): return "{n} x {part} from {supplier} (for {po})".format( n=decimal2string(self.quantity), part=self.part.SKU if self.part else 'unknown part', - supplier=self.order.supplier.name, + supplier=self.order.supplier.name if self.order.supplier else _('deleted'), po=self.order) order = models.ForeignKey( From f8a2811d9046bd0239ccab57e7b22c95513cd174 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 May 2022 00:55:29 +0200 Subject: [PATCH 36/85] change hierarchy --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 82cfa4e9e2..cf86e4cdc5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,7 +17,7 @@ The HEAD of the "main" or "master" branch of InvenTree represents the current "l **No pushing to master:** New featues must be submitted as a pull request from a separate branch (one branch per feature). -#### Feature Branches +### Feature Branches Feature branches should be branched *from* the *master* branch. @@ -45,7 +45,7 @@ The HEAD of the "stable" branch represents the latest stable release code. - The bugfix *must* also be cherry picked into the *master* branch. ## Environment -#### Target version +### Target version We are currently targeting: | Name | Minimum version | |---|---| @@ -65,7 +65,7 @@ pyupgrade `find . -name "*.py"` django-upgrade --target-version 3.2 `find . -name "*.py"` ``` -### Credits +## Credits If you add any new dependencies / libraries, they need to be added to [the docs](https://github.com/inventree/inventree-docs/blob/master/docs/credits.md). Please try to do that as timely as possible. From 1c0c3f9a729ce8da321cc59d725c64d548580367 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 May 2022 01:16:44 +0200 Subject: [PATCH 37/85] Add tags --- CONTRIBUTING.md | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cf86e4cdc5..3786a01ab8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -124,4 +124,44 @@ HTML and javascript files are passed through the django templating engine. Trans {% load i18n %} {% trans "This string will be translated" %} - this string will not! -``` \ No newline at end of file +``` + +## Github use +### Tags +The tags describe issues and PRs in multiple areas: +| Area | Name | Description | +|---|---|---| +| Inventree Features | | | +| | feat: API | tbd | +| | feat: barcode | tbd | +| | feat: build | tbd | +| | feat: docker | tbd | +| | feat: importer | tbd | +| | feat: order | tbd | +| | feat: part | tbd | +| | feat: plugin | tbd | +| | feat: pricing | tbd | +| | feat: report | tbd | +| | feat: setup | tbd | +| | feat: stock | tbd | +| | feat: user interface | tbd | +| Type | | | +| | typ: bug | tbd | +| | typ: dependencies | tbd | +| | typ: enhancement | tbd | +| | typ: security | tbd | +| | typ: question | tbd | +| | typ: roadmap | tbd | +| State | | | +| | state: duplicate | tbd | +| | state: invalid | tbd | +| | state: no-activity | tbd | +| | state: duplicate | tbd | +| | state: wontfix | tbd | +| Ecosystem | | | +| | eco: app | tbd | +| | eco: CI | tbd | +| | eco: demo | tbd | +| Built in | | | +| | help wanted | tbd | +| | starter | tbd | From e10ee91109372b3381576171ada869227e8ea415 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 May 2022 01:19:08 +0200 Subject: [PATCH 38/85] make names clearer --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3786a01ab8..145f50aa69 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -131,7 +131,7 @@ HTML and javascript files are passed through the django templating engine. Trans The tags describe issues and PRs in multiple areas: | Area | Name | Description | |---|---|---| -| Inventree Features | | | +| Feature | | | | | feat: API | tbd | | | feat: barcode | tbd | | | feat: build | tbd | @@ -162,6 +162,6 @@ The tags describe issues and PRs in multiple areas: | | eco: app | tbd | | | eco: CI | tbd | | | eco: demo | tbd | -| Built in | | | +| GH Built in | | | | | help wanted | tbd | | | starter | tbd | From f1d8bf71b7b5d344613994a393c40225e1475791 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 3 May 2022 10:18:11 +1000 Subject: [PATCH 39/85] Use the tablename when generating automatic model events --- InvenTree/plugin/events.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/InvenTree/plugin/events.py b/InvenTree/plugin/events.py index 4948366bfa..b54581bf71 100644 --- a/InvenTree/plugin/events.py +++ b/InvenTree/plugin/events.py @@ -163,17 +163,15 @@ def after_save(sender, instance, created, **kwargs): if created: trigger_event( - 'instance.created', + f'{table}.created', id=instance.id, model=sender.__name__, - table=table, ) else: trigger_event( - 'instance.saved', + f'{table}.saved', id=instance.id, model=sender.__name__, - table=table, ) @@ -189,9 +187,8 @@ def after_delete(sender, instance, **kwargs): return trigger_event( - 'instance.deleted', + f'{table}.deleted', model=sender.__name__, - table=table, ) From 4732efb330456c4ae9b1f3fe73a210b21115565b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 3 May 2022 11:42:13 +1000 Subject: [PATCH 40/85] Fix callbacks for adding new supplier part and/or purchase order "inline" --- InvenTree/company/api.py | 2 +- InvenTree/templates/js/translated/company.js | 8 +- .../js/translated/model_renderers.js | 4 +- InvenTree/templates/js/translated/order.js | 82 ++++++++----------- 4 files changed, 44 insertions(+), 52 deletions(-) diff --git a/InvenTree/company/api.py b/InvenTree/company/api.py index e4a589d9e5..b99fbd01fb 100644 --- a/InvenTree/company/api.py +++ b/InvenTree/company/api.py @@ -312,7 +312,7 @@ class SupplierPartList(generics.ListCreateAPIView): try: params = self.request.query_params kwargs['part_detail'] = str2bool(params.get('part_detail', None)) - kwargs['supplier_detail'] = str2bool(params.get('supplier_detail', None)) + kwargs['supplier_detail'] = str2bool(params.get('supplier_detail', True)) kwargs['manufacturer_detail'] = str2bool(params.get('manufacturer_detail', None)) kwargs['pretty'] = str2bool(params.get('pretty', None)) except AttributeError: diff --git a/InvenTree/templates/js/translated/company.js b/InvenTree/templates/js/translated/company.js index c92bb75d6f..d7b98841e4 100644 --- a/InvenTree/templates/js/translated/company.js +++ b/InvenTree/templates/js/translated/company.js @@ -115,10 +115,6 @@ function supplierPartFields() { return { part: {}, - supplier: {}, - SKU: { - icon: 'fa-hashtag', - }, manufacturer_part: { filters: { part_detail: true, @@ -126,6 +122,10 @@ function supplierPartFields() { }, auto_fill: true, }, + supplier: {}, + SKU: { + icon: 'fa-hashtag', + }, description: {}, link: { icon: 'fa-link', diff --git a/InvenTree/templates/js/translated/model_renderers.js b/InvenTree/templates/js/translated/model_renderers.js index 8fd6a6b3f4..d55d93e531 100644 --- a/InvenTree/templates/js/translated/model_renderers.js +++ b/InvenTree/templates/js/translated/model_renderers.js @@ -394,7 +394,9 @@ function renderSupplierPart(name, data, parameters={}, options={}) { html += select2Thumbnail(part_image); } - html += ` ${data.supplier_detail.name} - ${data.SKU}`; + if (data.supplier_detail) { + html += ` ${data.supplier_detail.name} - ${data.SKU}`; + } if (data.part_detail) { html += ` - ${data.part_detail.full_name}`; diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index 16b5b1f00d..72ec4197a5 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -260,8 +260,8 @@ function createPurchaseOrder(options={}) { } } }, - supplier_reference: {}, description: {}, + supplier_reference: {}, target_date: { icon: 'fa-calendar-alt', }, @@ -670,61 +670,51 @@ function orderParts(parts_list, options={}) { auto_fill: false, filters: { status: {{ PurchaseOrderStatus.PENDING }}, - }, - adjustFilters: function(query, opts) { - - // Whenever we open the drop-down to select an order, - // ensure we are only displaying orders which match the selected supplier part - var supplier_part_pk = getFormFieldValue(`supplier_part_${part.pk}`, opts); - - inventreeGet( - `/api/company/part/${supplier_part_pk}/`, - {}, - { - async: false, - success: function(data) { - query.supplier = data.supplier; - } - } - ); - - return query; + supplier_detail: true, }, noResults: function(query) { return '{% trans "No matching purchase orders" %}'; } }, null, opts); + }); - // Add callback for "remove row" button - $(opts.modal).find('.button-row-remove').click(function() { - var pk = $(this).attr('pk'); + // Add callback for "remove row" button + $(opts.modal).find('.button-row-remove').click(function() { + var pk = $(this).attr('pk'); - $(opts.modal).find(`#order_row_${pk}`).remove(); + $(opts.modal).find(`#order_row_${pk}`).remove(); + }); + + // Add callback for "new supplier part" button + $(opts.modal).find('.button-row-new-sp').click(function() { + var pk = $(this).attr('pk'); + + // Launch dialog to create new supplier part + createSupplierPart({ + part: pk, + onSuccess: function(response) { + setRelatedFieldData( + `supplier_part_${pk}`, + response, + opts + ); + } }); + }); - // Add callback for "new supplier part" button - $(opts.modal).find('.button-row-new-sp').click(function() { - var pk = $(this).attr('pk'); + // Add callback for "new purchase order" button + $(opts.modal).find('.button-row-new-po').click(function() { + var pk = $(this).attr('pk'); - // Launch dialog to create new supplier part - createSupplierPart({ - part: pk, - onSuccess: function(response) { - // TODO - } - }); - }); - - // Add callback for "new purchase order" button - $(opts.modal).find('.button-row-new-po').click(function() { - var pk = $(this).attr('pk'); - - // Launch dialog to create new purchase order - createPurchaseOrder({ - onSuccess: function(response) { - // TODO - } - }); + // Launch dialog to create new purchase order + createPurchaseOrder({ + onSuccess: function(response) { + setRelatedFieldData( + `purchase_order_${pk}`, + response, + opts + ); + } }); }); } From d69b5811b1fdda28df5c31230c065a8e4ebfd930 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 3 May 2022 12:00:28 +1000 Subject: [PATCH 41/85] Improved javascript log / warn / error messages --- InvenTree/templates/js/translated/api.js | 2 +- InvenTree/templates/js/translated/barcode.js | 1 - InvenTree/templates/js/translated/bom.js | 2 +- InvenTree/templates/js/translated/filters.js | 4 ++-- InvenTree/templates/js/translated/modals.js | 8 ++++---- InvenTree/templates/js/translated/order.js | 14 +++++++------- InvenTree/templates/js/translated/part.js | 2 +- InvenTree/templates/js/translated/tables.js | 4 ++-- 8 files changed, 18 insertions(+), 19 deletions(-) diff --git a/InvenTree/templates/js/translated/api.js b/InvenTree/templates/js/translated/api.js index fccd6bf5ef..119376c310 100644 --- a/InvenTree/templates/js/translated/api.js +++ b/InvenTree/templates/js/translated/api.js @@ -105,7 +105,7 @@ function inventreeFormDataUpload(url, data, options={}) { } }, error: function(xhr, status, error) { - console.log('Form data upload failure: ' + status); + console.error('Form data upload failure: ' + status); if (options.error) { options.error(xhr, status, error); diff --git a/InvenTree/templates/js/translated/barcode.js b/InvenTree/templates/js/translated/barcode.js index eb8a7f98b7..a6305eb1df 100644 --- a/InvenTree/templates/js/translated/barcode.js +++ b/InvenTree/templates/js/translated/barcode.js @@ -86,7 +86,6 @@ function onCameraAvailable(hasCamera, options) { function onBarcodeScanCompleted(result, options) { if (result.data == '') return; - console.log('decoded qr code:', result.data); stopQrScanner(); postBarcodeData(result.data, options); } diff --git a/InvenTree/templates/js/translated/bom.js b/InvenTree/templates/js/translated/bom.js index e4674d5989..7308583ae3 100644 --- a/InvenTree/templates/js/translated/bom.js +++ b/InvenTree/templates/js/translated/bom.js @@ -1061,7 +1061,7 @@ function loadBomTable(table, options={}) { table.bootstrapTable('append', response); }, error: function(xhr) { - console.log('Error requesting BOM for part=' + part_pk); + console.error('Error requesting BOM for part=' + part_pk); showApiError(xhr); } } diff --git a/InvenTree/templates/js/translated/filters.js b/InvenTree/templates/js/translated/filters.js index ceef79f66d..dd45aa6628 100644 --- a/InvenTree/templates/js/translated/filters.js +++ b/InvenTree/templates/js/translated/filters.js @@ -62,7 +62,7 @@ function loadTableFilters(tableKey) { if (f.length == 2) { filters[f[0]] = f[1]; } else { - console.log(`Improperly formatted filter: ${item}`); + console.warn(`Improperly formatted filter: ${item}`); } } }); @@ -274,7 +274,7 @@ function setupFilterList(tableKey, table, target, options={}) { var element = $(target); if (!element || !element.exists()) { - console.log(`WARNING: setupFilterList could not find target '${target}'`); + console.warn(`setupFilterList could not find target '${target}'`); return; } diff --git a/InvenTree/templates/js/translated/modals.js b/InvenTree/templates/js/translated/modals.js index f114f6f419..b72643a0d7 100644 --- a/InvenTree/templates/js/translated/modals.js +++ b/InvenTree/templates/js/translated/modals.js @@ -274,7 +274,7 @@ function reloadFieldOptions(fieldName, options) { setFieldOptions(fieldName, opts); }, error: function() { - console.log('Error GETting field options'); + console.error('Error GETting field options'); } }); } @@ -842,7 +842,7 @@ function attachFieldCallback(modal, callback) { // Run the callback function with the new value of the field! callback.action(field.val(), field); } else { - console.log(`Value changed for field ${callback.field} - ${field.val()}`); + console.info(`Value changed for field ${callback.field} - ${field.val()} (no callback attached)`); } }); } @@ -1085,8 +1085,8 @@ function launchModalForm(url, options = {}) { showAlertDialog('{% trans "Error requesting form data" %}', renderErrorMessage(xhr)); } - console.log('Modal form error: ' + xhr.status); - console.log('Message: ' + xhr.responseText); + console.error('Modal form error: ' + xhr.status); + console.info('Message: ' + xhr.responseText); } }; diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index 72ec4197a5..882657bb0a 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -1403,7 +1403,7 @@ function loadPurchaseOrderLineItemTable(table, options={}) { var line_item = $(table).bootstrapTable('getRowByUniqueId', pk); if (!line_item) { - console.log('WARNING: getRowByUniqueId returned null'); + console.warn('getRowByUniqueId returned null'); return; } @@ -1662,12 +1662,12 @@ function loadPurchaseOrderExtraLineTable(table, options={}) { options.params = options.params || {}; if (!options.order) { - console.log('ERROR: function called without order ID'); + console.error('function called without order ID'); return; } if (!options.status) { - console.log('ERROR: function called without order status'); + console.error('function called without order status'); return; } @@ -2789,12 +2789,12 @@ function loadSalesOrderLineItemTable(table, options={}) { options.params = options.params || {}; if (!options.order) { - console.log('ERROR: function called without order ID'); + console.error('function called without order ID'); return; } if (!options.status) { - console.log('ERROR: function called without order status'); + console.error('function called without order status'); return; } @@ -3297,12 +3297,12 @@ function loadSalesOrderExtraLineTable(table, options={}) { options.params = options.params || {}; if (!options.order) { - console.log('ERROR: function called without order ID'); + console.error('function called without order ID'); return; } if (!options.status) { - console.log('ERROR: function called without order status'); + console.error('function called without order status'); return; } diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js index 29ac95f149..8d76b833a6 100644 --- a/InvenTree/templates/js/translated/part.js +++ b/InvenTree/templates/js/translated/part.js @@ -876,7 +876,7 @@ function loadPartPurchaseOrderTable(table, part_id, options={}) { var line_item = $(table).bootstrapTable('getRowByUniqueId', pk); if (!line_item) { - console.log('WARNING: getRowByUniqueId returned null'); + console.warn('getRowByUniqueId returned null'); return; } diff --git a/InvenTree/templates/js/translated/tables.js b/InvenTree/templates/js/translated/tables.js index 8a6674299c..96c561b49b 100644 --- a/InvenTree/templates/js/translated/tables.js +++ b/InvenTree/templates/js/translated/tables.js @@ -39,7 +39,7 @@ function downloadTableData(table, opts={}) { var url = table_options.url; if (!url) { - console.log('Error: downloadTableData could not find "url" parameter.'); + console.error('downloadTableData could not find "url" parameter.'); } var query_params = table_options.query_params || {}; @@ -343,7 +343,7 @@ $.fn.inventreeTable = function(options) { } }); } else { - console.log(`Could not get list of visible columns for table '${tableName}'`); + console.error(`Could not get list of visible columns for table '${tableName}'`); } } From 1794f65d197dd02a75ecbdd544bbce903394d749 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 3 May 2022 13:44:28 +1000 Subject: [PATCH 42/85] Button to submit each row individually --- InvenTree/order/serializers.py | 1 + InvenTree/templates/js/translated/forms.js | 35 +++++---- InvenTree/templates/js/translated/order.js | 91 +++++++++++++++------- 3 files changed, 80 insertions(+), 47 deletions(-) diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index cc07dd3fea..eabc5e630e 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -246,6 +246,7 @@ class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer): total_price = serializers.FloatField(read_only=True) part_detail = PartBriefSerializer(source='get_base_part', many=False, read_only=True) + supplier_part_detail = SupplierPartSerializer(source='part', many=False, read_only=True) purchase_price = InvenTreeMoneySerializer( diff --git a/InvenTree/templates/js/translated/forms.js b/InvenTree/templates/js/translated/forms.js index 01f9e162eb..cc138052ef 100644 --- a/InvenTree/templates/js/translated/forms.js +++ b/InvenTree/templates/js/translated/forms.js @@ -1218,29 +1218,26 @@ function handleFormErrors(errors, fields={}, options={}) { for (var field_name in errors) { - if (field_name in fields) { + var field = fields[field_name] || {}; - var field = fields[field_name]; + if ((field.type == 'field') && ('child' in field)) { + // This is a "nested" field + handleNestedErrors(errors, field_name, options); + } else { + // This is a "simple" field - if ((field.type == 'field') && ('child' in field)) { - // This is a "nested" field - handleNestedErrors(errors, field_name, options); - } else { - // This is a "simple" field + var field_errors = errors[field_name]; - var field_errors = errors[field_name]; + if (field_errors && !first_error_field && isFieldVisible(field_name, options)) { + first_error_field = field_name; + } - if (field_errors && !first_error_field && isFieldVisible(field_name, options)) { - first_error_field = field_name; - } + // Add an entry for each returned error message + for (var ii = field_errors.length-1; ii >= 0; ii--) { - // Add an entry for each returned error message - for (var ii = field_errors.length-1; ii >= 0; ii--) { + var error_text = field_errors[ii]; - var error_text = field_errors[ii]; - - addFieldErrorMessage(field_name, error_text, ii, options); - } + addFieldErrorMessage(field_name, error_text, ii, options); } } } @@ -1929,6 +1926,10 @@ function renderModelData(name, model, data, parameters, options) { function getFieldName(name, options={}) { var field_name = name; + if (options.field_suffix) { + field_name += options.field_suffix; + } + if (options && options.depth) { field_name += `_${options.depth}`; } diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index 882657bb0a..042e5b785b 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -514,7 +514,7 @@ function orderParts(parts_list, options={}) { } var quantity_input = constructField( - `items_quantity_${pk}`, + `quantity_${pk}`, { type: 'decimal', min_value: 0, @@ -552,7 +552,7 @@ function orderParts(parts_list, options={}) { `; var purchase_order_input = constructField( - `purchase_order_${pk}`, + `order_${pk}`, { type: 'related field', required: true, @@ -565,16 +565,6 @@ function orderParts(parts_list, options={}) { var buttons = `
`; - buttons += makeIconButton( - 'fa-layer-group', - 'button-row-expand', - pk, - '{% trans "Expand Row" %}', - { - collapseTarget: `order_row_expand_${pk}`, - } - ); - if (parts.length > 1) { buttons += makeIconButton( 'fa-times icon-red', @@ -584,26 +574,23 @@ function orderParts(parts_list, options={}) { ); } + // Button to add row to purchase order + buttons += makeIconButton( + 'fa-shopping-cart icon-blue', + 'button-row-add', + pk, + '{% trans "Add to purchase order" %}', + ); + buttons += `
`; var html = ` - ${thumb} ${part.full_name} - ${supplier_part_input} - ${purchase_order_input} - ${quantity_input} - ${buttons} - `; - - // Add a second row "underneath" the first one, but collapsed - // Allows extra data to be added if required, but hidden by default - html += ` - - - reference goes here - - - + ${thumb} ${part.full_name} + ${supplier_part_input} + ${purchase_order_input} + ${quantity_input} + ${buttons} `; return html; @@ -662,7 +649,7 @@ function orderParts(parts_list, options={}) { // Configure the "purchase order" field initializeRelatedField({ - name: `purchase_order_${part.pk}`, + name: `order_${part.pk}`, model: 'purchaseorder', api_url: '{% url "api-po-list" %}', required: true, @@ -678,6 +665,50 @@ function orderParts(parts_list, options={}) { }, null, opts); }); + // Add callback for "add to purchase order" button + $(opts.modal).find('.button-row-add').click(function() { + var pk = $(this).attr('pk'); + + opts.field_suffix = null; + + // Extract information from the row + var data = { + quantity: getFormFieldValue(`quantity_${pk}`, {type: 'decimal',}, opts), + supplier_part: getFormFieldValue(`supplier_part_${pk}`, {}, opts), + order: getFormFieldValue(`order_${pk}`, {}, opts), + } + + // $(opts.modal).find(`#order_row_${pk}`).disable(); + // $(this).disable(); + + // Duplicate the form options, to prevent 'field_suffix' override + var row_opts = Object.assign(opts); + row_opts.field_suffix = `_${pk}`; + + inventreePut( + '{% url "api-po-line-list" %}', + data, + { + method: 'POST', + success: function(response) { + // Remove the row + $(opts.modal).find(`#order_row_${pk}`).remove(); + }, + error: function(xhr) { + switch (xhr.status) { + case 400: + handleFormErrors(xhr.responseJSON, fields, row_opts); + break; + default: + console.error(`Error adding line to purchase order`); + showApiError(xhr, options.url); + break; + } + } + } + ); + }); + // Add callback for "remove row" button $(opts.modal).find('.button-row-remove').click(function() { var pk = $(this).attr('pk'); @@ -710,7 +741,7 @@ function orderParts(parts_list, options={}) { createPurchaseOrder({ onSuccess: function(response) { setRelatedFieldData( - `purchase_order_${pk}`, + `order_${pk}`, response, opts ); From 8cd8581dbf8bdba49c765ce698b7582b7f4f4be9 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 3 May 2022 13:51:04 +1000 Subject: [PATCH 43/85] fixes --- InvenTree/order/serializers.py | 14 ++++++++++++-- InvenTree/templates/js/translated/order.js | 8 ++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index eabc5e630e..660a278cf5 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -268,8 +268,18 @@ class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer): data = super().validate(data) - supplier_part = data['part'] - purchase_order = data['order'] + supplier_part = data.get('part', None) + purchase_order = data.get('order', None) + + if not supplier_part: + raise ValidationError({ + 'part': _('Supplier part must be specified'), + }) + + if not purchase_order: + raise ValidationError({ + 'order': _('Purchase order must be specified'), + }) # Check that the supplier part and purchase order match if supplier_part is not None and supplier_part.supplier != purchase_order.supplier: diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index 042e5b785b..b17130d05b 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -534,7 +534,7 @@ function orderParts(parts_list, options={}) { `; var supplier_part_input = constructField( - `supplier_part_${pk}`, + `part_${pk}`, { type: 'related field', required: true, @@ -631,7 +631,7 @@ function orderParts(parts_list, options={}) { parts.forEach(function(part) { // Configure the "supplier part" field initializeRelatedField({ - name: `supplier_part_${part.pk}`, + name: `part_${part.pk}`, model: 'supplierpart', api_url: '{% url "api-supplier-part-list" %}', required: true, @@ -674,7 +674,7 @@ function orderParts(parts_list, options={}) { // Extract information from the row var data = { quantity: getFormFieldValue(`quantity_${pk}`, {type: 'decimal',}, opts), - supplier_part: getFormFieldValue(`supplier_part_${pk}`, {}, opts), + part: getFormFieldValue(`part_${pk}`, {}, opts), order: getFormFieldValue(`order_${pk}`, {}, opts), } @@ -725,7 +725,7 @@ function orderParts(parts_list, options={}) { part: pk, onSuccess: function(response) { setRelatedFieldData( - `supplier_part_${pk}`, + `part_${pk}`, response, opts ); From 141b764b9496caf24cb913cbc8c21ea5ef787c43 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 3 May 2022 14:00:43 +1000 Subject: [PATCH 44/85] Modal fixes --- InvenTree/order/serializers.py | 2 +- InvenTree/templates/js/translated/bom.js | 4 +-- InvenTree/templates/js/translated/modals.js | 37 ++++++++++++--------- InvenTree/templates/js/translated/order.js | 3 +- 4 files changed, 26 insertions(+), 20 deletions(-) diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 660a278cf5..7d26ce741d 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -246,7 +246,7 @@ class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer): total_price = serializers.FloatField(read_only=True) part_detail = PartBriefSerializer(source='get_base_part', many=False, read_only=True) - + supplier_part_detail = SupplierPartSerializer(source='part', many=False, read_only=True) purchase_price = InvenTreeMoneySerializer( diff --git a/InvenTree/templates/js/translated/bom.js b/InvenTree/templates/js/translated/bom.js index 7308583ae3..9bd66877da 100644 --- a/InvenTree/templates/js/translated/bom.js +++ b/InvenTree/templates/js/translated/bom.js @@ -129,7 +129,7 @@ function constructBomUploadTable(data, options={}) { var modal = createNewModal({ title: '{% trans "Row Data" %}', - cancelText: '{% trans "Close" %}', + closeText: '{% trans "Close" %}', hideSubmitButton: true }); @@ -617,7 +617,7 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) { }, }, preFormContent: html, - cancelText: '{% trans "Close" %}', + closeText: '{% trans "Close" %}', submitText: '{% trans "Add Substitute" %}', title: '{% trans "Edit BOM Item Substitutes" %}', afterRender: function(fields, opts) { diff --git a/InvenTree/templates/js/translated/modals.js b/InvenTree/templates/js/translated/modals.js index b72643a0d7..85f503682e 100644 --- a/InvenTree/templates/js/translated/modals.js +++ b/InvenTree/templates/js/translated/modals.js @@ -85,12 +85,25 @@ function createNewModal(options={}) { var modal_name = `#modal-form-${id}`; + // Callback *after* the modal has been rendered $(modal_name).on('shown.bs.modal', function() { $(modal_name + ' .modal-form-content').scrollTop(0); if (options.focus) { getFieldByName(modal_name, options.focus).focus(); } + + // Steal keyboard focus + $(modal_name).focus(); + + if (options.hideCloseButton) { + $(modal_name).find('#modal-form-cancel').hide(); + } + + if (options.preventSubmit || options.hideSubmitButton) { + $(modal_name).find('#modal-form-submit').hide(); + } + }); // Automatically remove the modal when it is deleted! @@ -102,8 +115,11 @@ function createNewModal(options={}) { $(modal_name).on('keydown', 'input', function(event) { if (event.keyCode == 13) { event.preventDefault(); - // Simulate a click on the 'Submit' button - $(modal_name).find('#modal-form-submit').click(); + + if (!options.preventSubmit) { + // Simulate a click on the 'Submit' button + $(modal_name).find('#modal-form-submit').click(); + } return false; } @@ -117,18 +133,7 @@ function createNewModal(options={}) { // Set labels based on supplied options modalSetTitle(modal_name, options.title || '{% trans "Form Title" %}'); modalSetSubmitText(modal_name, options.submitText || '{% trans "Submit" %}'); - modalSetCloseText(modal_name, options.cancelText || '{% trans "Cancel" %}'); - - if (options.hideSubmitButton) { - $(modal_name).find('#modal-form-submit').hide(); - } - - if (options.hideCloseButton) { - $(modal_name).find('#modal-form-cancel').hide(); - } - - // Steal keyboard focus - $(modal_name).focus(); + modalSetCloseText(modal_name, options.closeText || '{% trans "Cancel" %}'); // Return the "name" of the modal return modal_name; @@ -581,7 +586,7 @@ function showAlertDialog(title, content, options={}) { var modal = createNewModal({ title: title, - cancelText: '{% trans "Close" %}', + closeText: '{% trans "Close" %}', hideSubmitButton: true, }); @@ -607,7 +612,7 @@ function showQuestionDialog(title, content, options={}) { var modal = createNewModal({ title: title, submitText: options.accept_text || '{% trans "Accept" %}', - cancelText: options.cancel_text || '{% trans "Cancel" %}', + closeText: options.cancel_text || '{% trans "Cancel" %}', }); modalSetContent(modal, content); diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index b17130d05b..6c0c97cdce 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -625,7 +625,8 @@ function orderParts(parts_list, options={}) { constructFormBody({}, { preFormContent: html, title: '{% trans "Order Parts" %}', - hideSubmitButton: true, + preventSubmit: true, + closeText: '{% trans "Close" %}', afterRender: function(fields, opts) { // TODO parts.forEach(function(part) { From bac5a164919af264b821038b0b1e0713ff70cce7 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 3 May 2022 14:28:41 +1000 Subject: [PATCH 45/85] JS linting fxies --- InvenTree/templates/js/translated/order.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index 6c0c97cdce..7daaffff09 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -525,7 +525,7 @@ function orderParts(parts_list, options={}) { { hideLabels: true, } - ) + ); var supplier_part_prefix = ` @@ -674,13 +674,10 @@ function orderParts(parts_list, options={}) { // Extract information from the row var data = { - quantity: getFormFieldValue(`quantity_${pk}`, {type: 'decimal',}, opts), + quantity: getFormFieldValue(`quantity_${pk}`, {type: 'decimal'}, opts), part: getFormFieldValue(`part_${pk}`, {}, opts), order: getFormFieldValue(`order_${pk}`, {}, opts), - } - - // $(opts.modal).find(`#order_row_${pk}`).disable(); - // $(this).disable(); + }; // Duplicate the form options, to prevent 'field_suffix' override var row_opts = Object.assign(opts); From c0163a476f0a487852c311c035e4cd1797f87b6a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 3 May 2022 16:42:00 +1000 Subject: [PATCH 46/85] Refactor 'order parts' window from manufacturer part list --- .../company/templates/company/detail.html | 10 +++++++++- InvenTree/templates/js/translated/order.js | 18 +++++++++++++----- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/InvenTree/company/templates/company/detail.html b/InvenTree/company/templates/company/detail.html index 3d715e288c..8168c65609 100644 --- a/InvenTree/company/templates/company/detail.html +++ b/InvenTree/company/templates/company/detail.html @@ -325,9 +325,17 @@ var parts = []; selections.forEach(function(item) { - parts.push(item.part); + var part = item.part_detail; + part.manufacturer_part = item.pk; + parts.push(part); }); + orderParts( + parts, + ); + + return; + launchModalForm("/order/purchase-order/order-parts/", { data: { parts: parts, diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index 7daaffff09..d489ffd1c7 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -630,6 +630,18 @@ function orderParts(parts_list, options={}) { afterRender: function(fields, opts) { // TODO parts.forEach(function(part) { + + var filters = { + part: part.pk, + supplier_detail: true, + part_detail: true, + }; + + if (part.manufacturer_part) { + // Filter by manufacturer part + filters.manufacturer_part = part.manufacturer_part; + } + // Configure the "supplier part" field initializeRelatedField({ name: `part_${part.pk}`, @@ -638,11 +650,7 @@ function orderParts(parts_list, options={}) { required: true, type: 'related field', auto_fill: true, - filters: { - part: part.pk, - supplier_detail: true, - part_detail: false, - }, + filters: filters, noResults: function(query) { return '{% trans "No matching supplier parts" %}'; } From 28f3244574c0c77f05449bd560662124d7b586cb Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 3 May 2022 16:52:23 +1000 Subject: [PATCH 47/85] Order parts from "supplier parts" page - Prevent duplicate parts from loading --- InvenTree/build/templates/build/detail.html | 8 ---- .../company/templates/company/detail.html | 22 ++++----- InvenTree/templates/js/translated/order.js | 46 +++++++++++++------ 3 files changed, 41 insertions(+), 35 deletions(-) diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index 42bc51bb2f..2fb96e88e3 100644 --- a/InvenTree/build/templates/build/detail.html +++ b/InvenTree/build/templates/build/detail.html @@ -546,14 +546,6 @@ $('#allocate-selected-items').click(function() { ); }); -$("#btn-order-parts").click(function() { - launchModalForm("/order/purchase-order/order-parts/", { - data: { - build: {{ build.id }}, - }, - }); -}); - {% endif %} enableSidebar('buildorder'); diff --git a/InvenTree/company/templates/company/detail.html b/InvenTree/company/templates/company/detail.html index 8168c65609..4474278613 100644 --- a/InvenTree/company/templates/company/detail.html +++ b/InvenTree/company/templates/company/detail.html @@ -333,14 +333,6 @@ orderParts( parts, ); - - return; - - launchModalForm("/order/purchase-order/order-parts/", { - data: { - parts: parts, - }, - }); }); {% endif %} @@ -404,14 +396,16 @@ var parts = []; selections.forEach(function(item) { - parts.push(item.part); + var part = item.part_detail; + parts.push(part); }); - launchModalForm("/order/purchase-order/order-parts/", { - data: { - parts: parts, - }, - }); + orderParts( + parts, + { + supplier: {{ company.pk }}, + } + ); }); {% endif %} diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index d489ffd1c7..1b197a2766 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -485,9 +485,16 @@ function orderParts(parts_list, options={}) { var parts = []; + var parts_seen = {}; + parts_list.forEach(function(part) { if (part.purchaseable) { - parts.push(part); + + // Prevent duplicates + if (!(part.pk in parts_seen)) { + parts_seen[part.pk] = true; + parts.push(part); + } } }); @@ -622,24 +629,40 @@ function orderParts(parts_list, options={}) { `; + // Construct API filters for the SupplierPart field + var supplier_part_filters = { + supplier_detail: true, + part_detail: true, + }; + + if (options.supplier) { + supplier_part_filters.supplier = options.supplier; + } + + // Construct API filtres for the PurchaseOrder field + var order_filters = { + status: {{ PurchaseOrderStatus.PENDING }}, + supplier_detail: true, + }; + + if (options.supplier) { + order_filters.supplier = options.supplier; + } + constructFormBody({}, { preFormContent: html, title: '{% trans "Order Parts" %}', preventSubmit: true, closeText: '{% trans "Close" %}', afterRender: function(fields, opts) { - // TODO parts.forEach(function(part) { - var filters = { - part: part.pk, - supplier_detail: true, - part_detail: true, - }; + // Filter by base part + supplier_part_filters.part = part.pk; if (part.manufacturer_part) { // Filter by manufacturer part - filters.manufacturer_part = part.manufacturer_part; + supplier_part_filters.manufacturer_part = part.manufacturer_part; } // Configure the "supplier part" field @@ -650,7 +673,7 @@ function orderParts(parts_list, options={}) { required: true, type: 'related field', auto_fill: true, - filters: filters, + filters: supplier_part_filters, noResults: function(query) { return '{% trans "No matching supplier parts" %}'; } @@ -664,10 +687,7 @@ function orderParts(parts_list, options={}) { required: true, type: 'related field', auto_fill: false, - filters: { - status: {{ PurchaseOrderStatus.PENDING }}, - supplier_detail: true, - }, + filters: order_filters, noResults: function(query) { return '{% trans "No matching purchase orders" %}'; } From 05b01e4d6c8151d517e2bffb6aebc781c75d1a51 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 3 May 2022 17:16:38 +1000 Subject: [PATCH 48/85] Refactor order parts for manfuacturer part detail page --- .../templates/company/manufacturer_part.html | 25 +++++++++++-------- InvenTree/templates/js/translated/order.js | 8 ++++++ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/InvenTree/company/templates/company/manufacturer_part.html b/InvenTree/company/templates/company/manufacturer_part.html index fb33128a77..a3a2bbc65e 100644 --- a/InvenTree/company/templates/company/manufacturer_part.html +++ b/InvenTree/company/templates/company/manufacturer_part.html @@ -31,13 +31,11 @@ {% include "admin_button.html" with url=url %} {% endif %} {% if roles.purchase_order.change %} -{% comment "for later" %} -{% if roles.purchase_order.add %} +{% if roles.purchase_order.add and part.part.purchaseable %} {% endif %} -{% endcomment %} @@ -130,6 +128,7 @@ src="{% static 'img/blank_image.png' %}"
  • {% trans "Delete" %}
  • + {% include "filter_list.html" with id='supplier-part' %} @@ -300,14 +299,20 @@ linkButtonsToSelection($("#supplier-table"), ['#supplier-part-options']); linkButtonsToSelection($("#parameter-table"), ['#parameter-options']); $('#order-part, #order-part2').click(function() { - launchModalForm( - "{% url 'order-parts' %}", + + inventreeGet( + '{% url "api-part-detail" part.part.pk %}', {}, { - data: { - part: {{ part.part.id }}, - }, - reload: true, - }, + success: function(response) { + + orderParts([response], { + manufacturer_part: {{ part.pk }}, + {% if part.manufacturer %} + manufacturer: {{ part.manufacturer.pk }}, + {% endif %} + }); + } + } ); }); diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index 1b197a2766..214a6fa885 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -639,6 +639,14 @@ function orderParts(parts_list, options={}) { supplier_part_filters.supplier = options.supplier; } + if (options.manufacturer) { + supplier_part_filters.manufacturer = options.manufacturer; + } + + if (options.manufacturer_part) { + supplier_part_filters.manufacturer_part = options.manufacturer_part; + } + // Construct API filtres for the PurchaseOrder field var order_filters = { status: {{ PurchaseOrderStatus.PENDING }}, From f3e8edaf1ffda2773962f953ed35391679369e3a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 3 May 2022 17:21:33 +1000 Subject: [PATCH 49/85] Refactor for SupplierPart detail page --- .../templates/company/supplier_part.html | 22 ++++++++++++------- InvenTree/templates/js/translated/order.js | 2 ++ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/InvenTree/company/templates/company/supplier_part.html b/InvenTree/company/templates/company/supplier_part.html index 67902dc6f6..250c595476 100644 --- a/InvenTree/company/templates/company/supplier_part.html +++ b/InvenTree/company/templates/company/supplier_part.html @@ -165,7 +165,8 @@ src="{% static 'img/blank_image.png' %}"
    -
    +
    + {% include "filter_list.html" with id='purchaseorder' %}
    @@ -326,14 +327,19 @@ $("#item-create").click(function() { }); $('#order-part, #order-part2').click(function() { - launchModalForm( - "{% url 'order-parts' %}", + + inventreeGet( + '{% url "api-part-detail" part.part.pk %}', {}, { - data: { - part: {{ part.part.id }}, - }, - reload: true, - }, + success: function(response) { + orderParts([response], { + supplier_part: {{ part.pk }}, + {% if part.supplier %} + supplier: {{ part.supplier.pk }}, + {% endif %} + }); + } + } ); }); diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index 214a6fa885..9b8366a5aa 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -681,6 +681,7 @@ function orderParts(parts_list, options={}) { required: true, type: 'related field', auto_fill: true, + value: options.supplier_part, filters: supplier_part_filters, noResults: function(query) { return '{% trans "No matching supplier parts" %}'; @@ -695,6 +696,7 @@ function orderParts(parts_list, options={}) { required: true, type: 'related field', auto_fill: false, + value: options.order, filters: order_filters, noResults: function(query) { return '{% trans "No matching purchase orders" %}'; From f0e8e32c8a0ae4d1c605efcb2b1fca108f4adbc9 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 3 May 2022 17:27:21 +1000 Subject: [PATCH 50/85] More refactoring --- InvenTree/part/templates/part/detail.html | 18 ++++++++++++------ InvenTree/part/templates/part/part_base.html | 9 --------- InvenTree/templates/js/translated/order.js | 16 +++++++++++++--- 3 files changed, 25 insertions(+), 18 deletions(-) diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index 5ec1821b3d..aa3ad4963a 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -754,12 +754,18 @@ $("#part-order2").click(function() { - launchModalForm("{% url 'order-parts' %}", { - data: { - part: {{ part.id }}, - }, - reload: true, - }); + inventreeGet( + '{% url "api-part-detail" part.pk %}', + {}, + { + success: function(part) { + orderParts( + [part], + {} + ); + } + } + ); }); onPanelLoad("test-templates", function() { diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index 649aaf6705..4e875c1f97 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -549,15 +549,6 @@ } } ); - - return; - - launchModalForm("{% url 'order-parts' %}", { - data: { - part: {{ part.id }}, - }, - reload: true, - }); }); {% if roles.part.add %} diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index 9b8366a5aa..496ea6a61e 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -603,6 +603,17 @@ function orderParts(parts_list, options={}) { return html; } + // Remove a single row form this dialog + function removeRow(pk, opts) { + // Remove the row + $(opts.modal).find(`#order_row_${pk}`).remove(); + + // If the modal is now "empty", dismiss it + if (!($(opts.modal).find('.part-order-row').exists())) { + closeModal(opts.modal); + } + } + var table_entries = ''; parts.forEach(function(part) { @@ -727,8 +738,7 @@ function orderParts(parts_list, options={}) { { method: 'POST', success: function(response) { - // Remove the row - $(opts.modal).find(`#order_row_${pk}`).remove(); + removeRow(pk, opts); }, error: function(xhr) { switch (xhr.status) { @@ -749,7 +759,7 @@ function orderParts(parts_list, options={}) { $(opts.modal).find('.button-row-remove').click(function() { var pk = $(this).attr('pk'); - $(opts.modal).find(`#order_row_${pk}`).remove(); + removeRow(pk, opts); }); // Add callback for "new supplier part" button From 96c5a8252c9793cc0f6c88aad4fd43496f04cae8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 3 May 2022 17:33:03 +1000 Subject: [PATCH 51/85] remove dead code --- .../order/order_wizard/select_parts.html | 85 ----- .../order/order_wizard/select_pos.html | 77 ---- InvenTree/order/urls.py | 3 +- InvenTree/order/views.py | 340 ------------------ InvenTree/templates/js/translated/build.js | 17 +- InvenTree/templates/js/translated/order.js | 19 +- InvenTree/templates/js/translated/stock.js | 14 +- 7 files changed, 31 insertions(+), 524 deletions(-) delete mode 100644 InvenTree/order/templates/order/order_wizard/select_parts.html delete mode 100644 InvenTree/order/templates/order/order_wizard/select_pos.html diff --git a/InvenTree/order/templates/order/order_wizard/select_parts.html b/InvenTree/order/templates/order/order_wizard/select_parts.html deleted file mode 100644 index 9d0ccdfb82..0000000000 --- a/InvenTree/order/templates/order/order_wizard/select_parts.html +++ /dev/null @@ -1,85 +0,0 @@ -{% extends "modal_form.html" %} - -{% load inventree_extras %} -{% load i18n %} - -{% block form %} -{% default_currency as currency %} -{% settings_value 'PART_SHOW_PRICE_IN_FORMS' as show_price %} - -

    - {% trans "Step 1 of 2 - Select Part Suppliers" %} -

    - -{% if parts|length > 0 %} - -{% else %} - -{% endif %} - -
    - {% csrf_token %} - {% load crispy_forms_tags %} - - - -
    - - - - - - - {% for part in parts %} - - - - - - - - {% endfor %} -
    {% trans "Part" %}{% trans "Select Supplier" %}{% trans "Quantity" %}
    - {% include "hover_image.html" with image=part.image hover=False %} - {{ part.full_name }} {{ part.description }} - - - -
    -
    - -
    - {% if not part.order_supplier %} - {% blocktrans with name=part.name %}Select a supplier for {{name}}{% endblocktrans %} - {% endif %} -
    -
    -
    -
    - -
    -
    -
    - -
    - -{% endblock %} \ No newline at end of file diff --git a/InvenTree/order/templates/order/order_wizard/select_pos.html b/InvenTree/order/templates/order/order_wizard/select_pos.html deleted file mode 100644 index 6ef2f6c910..0000000000 --- a/InvenTree/order/templates/order/order_wizard/select_pos.html +++ /dev/null @@ -1,77 +0,0 @@ -{% extends "modal_form.html" %} - -{% load i18n %} - -{% block form %} - -

    - {% trans "Step 2 of 2 - Select Purchase Orders" %} -

    - - - -
    - {% csrf_token %} - {% load crispy_forms_tags %} - - - - {% for supplier in suppliers %} - {% for item in supplier.order_items %} - - - {% endfor %} - {% endfor %} - - - - - - - - {% for supplier in suppliers %} - - - - - - - {% endfor %} - -
    {% trans "Supplier" %}{% trans "Items" %}{% trans "Select Purchase Order" %}
    - {% include 'hover_image.html' with image=supplier.image hover=False %} - {{ supplier.name }} - {{ supplier.order_items|length }} - - -
    -
    - -
    - {% if not supplier.selected_purchase_order %} - {% blocktrans with name=supplier.name %}Select a purchase order for {{name}}{% endblocktrans %} - {% endif %} -
    -
    -
    -{% endblock %} \ No newline at end of file diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py index 54be93905f..a2a2897da2 100644 --- a/InvenTree/order/urls.py +++ b/InvenTree/order/urls.py @@ -22,8 +22,7 @@ purchase_order_detail_urls = [ ] purchase_order_urls = [ - - re_path(r'^order-parts/', views.OrderParts.as_view(), name='order-parts'), + re_path(r'^pricing/', views.LineItemPricing.as_view(), name='line-pricing'), # Display detail view for a single purchase order diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 35f8b973f4..15bff617d1 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -448,346 +448,6 @@ class PurchaseOrderExport(AjaxView): return DownloadFile(filedata, filename) -class OrderParts(AjaxView): - """ View for adding various SupplierPart items to a Purchase Order. - - SupplierParts can be selected from a variety of 'sources': - - - ?supplier_parts[]= -> Direct list of SupplierPart objects - - ?parts[]= -> List of base Part objects (user must then select supplier parts) - - ?stock[]= -> List of StockItem objects (user must select supplier parts) - - ?build= -> A Build object (user must select parts, then supplier parts) - - """ - - ajax_form_title = _("Order Parts") - ajax_template_name = 'order/order_wizard/select_parts.html' - - role_required = [ - 'part.view', - 'purchase_order.change', - ] - - # List of Parts we wish to order - parts = [] - suppliers = [] - - def get_context_data(self): - - ctx = {} - - ctx['parts'] = sorted(self.parts, key=lambda part: int(part.order_quantity), reverse=True) - ctx['suppliers'] = self.suppliers - - return ctx - - def get_data(self): - """ enrich respone json data """ - data = super().get_data() - # if in selection-phase, add a button to update the prices - if getattr(self, 'form_step', 'select_parts') == 'select_parts': - data['buttons'] = [{'name': 'update_price', 'title': _('Update prices')}] # set buttons - data['hideErrorMessage'] = '1' # hide the error message - return data - - def get_suppliers(self): - """ Calculates a list of suppliers which the user will need to create PurchaseOrders for. - This is calculated AFTER the user finishes selecting the parts to order. - Crucially, get_parts() must be called before get_suppliers() - """ - - suppliers = {} - - for supplier in self.suppliers: - supplier.order_items = [] - - suppliers[supplier.name] = supplier - - for part in self.parts: - supplier_part_id = part.order_supplier - - try: - supplier = SupplierPart.objects.get(pk=supplier_part_id).supplier - except SupplierPart.DoesNotExist: - continue - - if supplier.name not in suppliers: - supplier.order_items = [] - - # Attempt to auto-select a purchase order - orders = PurchaseOrder.objects.filter(supplier=supplier, status__in=PurchaseOrderStatus.OPEN) - - if orders.count() == 1: - supplier.selected_purchase_order = orders.first().id - else: - supplier.selected_purchase_order = None - - suppliers[supplier.name] = supplier - - suppliers[supplier.name].order_items.append(part) - - self.suppliers = [suppliers[key] for key in suppliers.keys()] - - def get_parts(self): - """ Determine which parts the user wishes to order. - This is performed on the initial GET request. - """ - - self.parts = [] - - part_ids = set() - - # User has passed a list of stock items - if 'stock[]' in self.request.GET: - - stock_id_list = self.request.GET.getlist('stock[]') - - """ Get a list of all the parts associated with the stock items. - - Base part must be purchaseable. - - Return a set of corresponding Part IDs - """ - stock_items = StockItem.objects.filter( - part__purchaseable=True, - id__in=stock_id_list) - - for item in stock_items: - part_ids.add(item.part.id) - - # User has passed a single Part ID - elif 'part' in self.request.GET: - try: - part_id = self.request.GET.get('part') - part = Part.objects.get(id=part_id) - - part_ids.add(part.id) - - except Part.DoesNotExist: - pass - - # User has passed a list of part ID values - elif 'parts[]' in self.request.GET: - part_id_list = self.request.GET.getlist('parts[]') - - parts = Part.objects.filter( - purchaseable=True, - id__in=part_id_list) - - for part in parts: - part_ids.add(part.id) - - # User has provided a Build ID - elif 'build' in self.request.GET: - build_id = self.request.GET.get('build') - try: - build = Build.objects.get(id=build_id) - - parts = build.required_parts - - for part in parts: - - # If ordering from a Build page, ignore parts that we have enough of - if part.quantity_to_order <= 0: - continue - part_ids.add(part.id) - except Build.DoesNotExist: - pass - - # Create the list of parts - for id in part_ids: - try: - part = Part.objects.get(id=id) - # Pre-fill the 'order quantity' value - part.order_quantity = part.quantity_to_order - - default_supplier = part.get_default_supplier() - - if default_supplier: - part.order_supplier = default_supplier.id - else: - part.order_supplier = None - except Part.DoesNotExist: - continue - - self.parts.append(part) - - def get(self, request, *args, **kwargs): - - self.request = request - - self.get_parts() - - return self.renderJsonResponse(request) - - def post(self, request, *args, **kwargs): - """ Handle the POST action for part selection. - - - Validates each part / quantity / supplier / etc - - Part selection form contains the following fields for each part: - - - supplier- : The ID of the selected supplier - - quantity- : The quantity to add to the order - """ - - self.request = request - - self.parts = [] - self.suppliers = [] - - # Any errors for the part selection form? - part_errors = False - supplier_errors = False - - # Extract part information from the form - for item in self.request.POST: - - if item.startswith('part-supplier-'): - - pk = item.replace('part-supplier-', '') - - # Check that the part actually exists - try: - part = Part.objects.get(id=pk) - except (Part.DoesNotExist, ValueError): - continue - - supplier_part_id = self.request.POST[item] - - quantity = self.request.POST.get('part-quantity-' + str(pk), 0) - - # Ensure a valid supplier has been passed - try: - supplier_part = SupplierPart.objects.get(id=supplier_part_id) - except (SupplierPart.DoesNotExist, ValueError): - supplier_part = None - - # Ensure a valid quantity is passed - try: - quantity = int(quantity) - - # Eliminate lines where the quantity is zero - if quantity == 0: - continue - except ValueError: - quantity = part.quantity_to_order - - part.order_supplier = supplier_part.id if supplier_part else None - part.order_quantity = quantity - - # set supplier-price - if supplier_part: - supplier_price = supplier_part.get_price(quantity) - if supplier_price: - part.purchase_price = supplier_price / quantity - if not hasattr(part, 'purchase_price'): - part.purchase_price = None - - self.parts.append(part) - - if supplier_part is None: - part_errors = True - - elif quantity < 0: - part_errors = True - - elif item.startswith('purchase-order-'): - # Which purchase order is selected for a given supplier? - pk = item.replace('purchase-order-', '') - - # Check that the Supplier actually exists - try: - supplier = Company.objects.get(id=pk) - except Company.DoesNotExist: - # Skip this item - continue - - purchase_order_id = self.request.POST[item] - - # Ensure that a valid purchase order has been passed - try: - purchase_order = PurchaseOrder.objects.get(pk=purchase_order_id) - except (PurchaseOrder.DoesNotExist, ValueError): - purchase_order = None - - supplier.selected_purchase_order = purchase_order.id if purchase_order else None - - self.suppliers.append(supplier) - - if supplier.selected_purchase_order is None: - supplier_errors = True - - form_step = request.POST.get('form_step') - - # Map parts to suppliers - self.get_suppliers() - - valid = False - - if form_step == 'select_parts': - # No errors? and the price-update button was not used to submit? Proceed to PO selection form - if part_errors is False and 'act-btn_update_price' not in request.POST: - self.ajax_template_name = 'order/order_wizard/select_pos.html' - self.form_step = 'select_purchase_orders' # set step (important for get_data) - - else: - self.ajax_template_name = 'order/order_wizard/select_parts.html' - - elif form_step == 'select_purchase_orders': - - self.ajax_template_name = 'order/order_wizard/select_pos.html' - - valid = part_errors is False and supplier_errors is False - - # Form wizard is complete! Add items to purchase orders - if valid: - self.order_items() - - data = { - 'form_valid': valid, - 'success': _('Ordered {n} parts').format(n=len(self.parts)) - } - - return self.renderJsonResponse(self.request, data=data) - - @transaction.atomic - def order_items(self): - """ Add the selected items to the purchase orders. """ - - for supplier in self.suppliers: - - # Check that the purchase order does actually exist - try: - order = PurchaseOrder.objects.get(pk=supplier.selected_purchase_order) - except PurchaseOrder.DoesNotExist: - logger.critical('Could not add items to purchase order {po} - Order does not exist'.format(po=supplier.selected_purchase_order)) - continue - - for item in supplier.order_items: - - # Ensure that the quantity is valid - try: - quantity = int(item.order_quantity) - if quantity <= 0: - continue - except ValueError: - logger.warning("Did not add part to purchase order - incorrect quantity") - continue - - # Check that the supplier part does actually exist - try: - supplier_part = SupplierPart.objects.get(pk=item.order_supplier) - except SupplierPart.DoesNotExist: - logger.critical("Could not add part '{part}' to purchase order - selected supplier part '{sp}' does not exist.".format( - part=item, - sp=item.order_supplier)) - continue - - # get purchase price - purchase_price = item.purchase_price - - order.add_line_item(supplier_part, quantity, purchase_price=purchase_price) - - class LineItemPricing(PartPricing): """ View for inspecting part pricing information """ diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index c9ebbe0e22..d68b319a25 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -1532,13 +1532,18 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { var pk = $(this).attr('pk'); - launchModalForm('{% url "order-parts" %}', { - data: { - parts: [ - pk, - ] + inventreeGet( + `/api/part/${pk}/`, + {}, + { + success: function(part) { + orderParts( + [part], + {} + ); + } } - }); + ); }); // Callback for 'build' button diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index 496ea6a61e..d5ca7caf42 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -3290,13 +3290,18 @@ function loadSalesOrderLineItemTable(table, options={}) { $(table).find('.button-buy').click(function() { var pk = $(this).attr('pk'); - launchModalForm('{% url "order-parts" %}', { - data: { - parts: [ - pk - ], - }, - }); + inventreeGet( + `/api/part/${pk}/`, + {}, + { + success: function(part) { + orderParts( + [part], + {} + ); + } + } + ); }); // Callback for displaying price diff --git a/InvenTree/templates/js/translated/stock.js b/InvenTree/templates/js/translated/stock.js index de42528142..94d21fe5b0 100644 --- a/InvenTree/templates/js/translated/stock.js +++ b/InvenTree/templates/js/translated/stock.js @@ -2043,17 +2043,17 @@ function loadStockTable(table, options) { $('#multi-item-order').click(function() { var selections = $(table).bootstrapTable('getSelections'); - var stock = []; + var parts = []; selections.forEach(function(item) { - stock.push(item.pk); + var part = item.part_detail; + + if (part) { + parts.push(part); + } }); - launchModalForm('/order/purchase-order/order-parts/', { - data: { - stock: stock, - }, - }); + orderParts(parts, {}); }); $('#multi-item-set-status').click(function() { From e7ddeb8173f7f69a9fcf274c65dd98b42e7a53c0 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 3 May 2022 17:36:30 +1000 Subject: [PATCH 52/85] PEP style fixes --- InvenTree/order/urls.py | 2 +- InvenTree/order/views.py | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py index a2a2897da2..f82a581828 100644 --- a/InvenTree/order/urls.py +++ b/InvenTree/order/urls.py @@ -22,7 +22,7 @@ purchase_order_detail_urls = [ ] purchase_order_urls = [ - + re_path(r'^pricing/', views.LineItemPricing.as_view(), name='line-pricing'), # Display detail view for a single purchase order diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 15bff617d1..68b45ebe86 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -5,7 +5,6 @@ Django views for interacting with Order app # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import transaction from django.db.utils import IntegrityError from django.http.response import JsonResponse from django.shortcuts import get_object_or_404 @@ -21,9 +20,7 @@ from decimal import Decimal, InvalidOperation from .models import PurchaseOrder, PurchaseOrderLineItem from .models import SalesOrder, SalesOrderLineItem from .admin import PurchaseOrderLineItemResource, SalesOrderLineItemResource -from build.models import Build -from company.models import Company, SupplierPart # ManufacturerPart -from stock.models import StockItem +from company.models import SupplierPart # ManufacturerPart from part.models import Part from common.forms import UploadFileForm, MatchFieldForm @@ -37,8 +34,6 @@ from InvenTree.views import AjaxView, AjaxUpdateView from InvenTree.helpers import DownloadFile, str2bool from InvenTree.views import InvenTreeRoleMixin -from InvenTree.status_codes import PurchaseOrderStatus - logger = logging.getLogger("inventree") From 0900fe82dcbd71f41472292002ac8bc8a49fc0fa Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 May 2022 20:11:41 +0200 Subject: [PATCH 53/85] ignore coverage on reverse conversion --- .../0064_purchaseorderextraline_salesorderextraline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/order/migrations/0064_purchaseorderextraline_salesorderextraline.py b/InvenTree/order/migrations/0064_purchaseorderextraline_salesorderextraline.py index 53bf0621ed..1c3d2ff743 100644 --- a/InvenTree/order/migrations/0064_purchaseorderextraline_salesorderextraline.py +++ b/InvenTree/order/migrations/0064_purchaseorderextraline_salesorderextraline.py @@ -35,7 +35,7 @@ def _convert_model(apps, line_item_ref, extra_line_ref, price_ref): print(f'Done converting line items - now at {OrderExtraLine.objects.all().count()} {extra_line_ref} / {OrderLineItem.objects.all().count()} {line_item_ref} instance(s)') -def _reconvert_model(apps, line_item_ref, extra_line_ref): +def _reconvert_model(apps, line_item_ref, extra_line_ref): # pragma: no cover """Convert ExtraLine instances back to OrderLineItem instances""" OrderLineItem = apps.get_model('order', line_item_ref) OrderExtraLine = apps.get_model('order', extra_line_ref) From 05d2d3664c75bc9e982a2aeb18219ffbe9b0848e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 May 2022 20:13:07 +0200 Subject: [PATCH 54/85] ignore defaults for coverage --- InvenTree/plugin/builtin/barcode/mixins.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/InvenTree/plugin/builtin/barcode/mixins.py b/InvenTree/plugin/builtin/barcode/mixins.py index 693df4b662..417ca04bcd 100644 --- a/InvenTree/plugin/builtin/barcode/mixins.py +++ b/InvenTree/plugin/builtin/barcode/mixins.py @@ -69,7 +69,7 @@ class BarcodeMixin: Default implementation returns None """ - return None + return None # pragma: no cover def getStockItemByHash(self): """ @@ -97,7 +97,7 @@ class BarcodeMixin: Default implementation returns None """ - return None + return None # pragma: no cover def renderStockLocation(self, loc): """ @@ -113,7 +113,7 @@ class BarcodeMixin: Default implementation returns None """ - return None + return None # pragma: no cover def renderPart(self, part): """ @@ -143,4 +143,4 @@ class BarcodeMixin: """ Default implementation returns False """ - return False + return False # pragma: no cover From 300558adb06994b6884ef919e7376c4c1593eca2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 May 2022 20:26:49 +0200 Subject: [PATCH 55/85] increase coverage on owners model --- InvenTree/users/tests.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/InvenTree/users/tests.py b/InvenTree/users/tests.py index d9af560ed8..e6a4019481 100644 --- a/InvenTree/users/tests.py +++ b/InvenTree/users/tests.py @@ -197,6 +197,10 @@ class OwnerModelTest(TestCase): self.assertTrue(user_as_owner in related_owners) self.assertTrue(group_as_owner in related_owners) + # Check owner matching + owners = Owner.get_owners_matching_user(self.user) + self.assertEqual(owners, [user_as_owner, group_as_owner]) + # Delete user and verify owner was deleted too self.user.delete() user_as_owner = Owner.get_owner(self.user) From 5fa7b45d1dcb295a1d76fa0bf9c6994d656b76b7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 May 2022 21:39:45 +0200 Subject: [PATCH 56/85] Add tests for scheduling Closes #2523 --- .../integration/test_scheduled_task.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 InvenTree/plugin/samples/integration/test_scheduled_task.py diff --git a/InvenTree/plugin/samples/integration/test_scheduled_task.py b/InvenTree/plugin/samples/integration/test_scheduled_task.py new file mode 100644 index 0000000000..4df357df29 --- /dev/null +++ b/InvenTree/plugin/samples/integration/test_scheduled_task.py @@ -0,0 +1,29 @@ +""" Unit tests for scheduled tasks""" + +from django.test import TestCase + +from plugin import registry + + +class ScheduledTaskPluginTests(TestCase): + """ Tests for ScheduledTaskPlugin """ + + def test_function(self): + """check if the scheduling works""" + # The plugin should be defined + self.assertIn('schedule', registry.plugins) + plg = registry.plugins['schedule'] + self.assertTrue(plg) + + # check that the built-in function is running + plg.member_func() + + # check that the tasks are defined + self.assertEqual(plg.get_task_names(), ['plugin.schedule.member', 'plugin.schedule.hello', 'plugin.schedule.world']) + + # register + plg.register_tasks() + # check that schedule was registers + from django_q.models import Schedule + scheduled_plugin_tasks = Schedule.objects.filter(name__istartswith="plugin.") + self.assertEqual(len(scheduled_plugin_tasks), 3) From 5ea6a325ee1e2f0c72f30f017eba8baf9d2c2454 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 May 2022 21:48:49 +0200 Subject: [PATCH 57/85] fix smaple code --- InvenTree/plugin/samples/integration/scheduled_task.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/samples/integration/scheduled_task.py b/InvenTree/plugin/samples/integration/scheduled_task.py index c8b1c4c5d0..579ad4effe 100644 --- a/InvenTree/plugin/samples/integration/scheduled_task.py +++ b/InvenTree/plugin/samples/integration/scheduled_task.py @@ -36,7 +36,7 @@ class ScheduledTaskPlugin(ScheduleMixin, SettingsMixin, IntegrationPluginBase): 'minutes': 45, }, 'world': { - 'func': 'plugin.samples.integration.scheduled_task.print_hello', + 'func': 'plugin.samples.integration.scheduled_task.print_world', 'schedule': 'H', }, } From 70b108d81305a261dd657735b53eed3886f6d01c Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 May 2022 21:56:58 +0200 Subject: [PATCH 58/85] add check for call function --- InvenTree/plugin/samples/integration/test_scheduled_task.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/InvenTree/plugin/samples/integration/test_scheduled_task.py b/InvenTree/plugin/samples/integration/test_scheduled_task.py index 4df357df29..a43356c36d 100644 --- a/InvenTree/plugin/samples/integration/test_scheduled_task.py +++ b/InvenTree/plugin/samples/integration/test_scheduled_task.py @@ -3,6 +3,7 @@ from django.test import TestCase from plugin import registry +from plugin.registry import call_function class ScheduledTaskPluginTests(TestCase): @@ -27,3 +28,7 @@ class ScheduledTaskPluginTests(TestCase): from django_q.models import Schedule scheduled_plugin_tasks = Schedule.objects.filter(name__istartswith="plugin.") self.assertEqual(len(scheduled_plugin_tasks), 3) + + def test_calling(self): + """check if a function can be called without errors""" + call_function('schedule', 'member_func') From 7c437a3cf293e4d78e4fd1dd6a036ab9058e55db Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 May 2022 21:59:17 +0200 Subject: [PATCH 59/85] adjust testing function to also check return --- InvenTree/plugin/samples/integration/scheduled_task.py | 1 + InvenTree/plugin/samples/integration/test_scheduled_task.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugin/samples/integration/scheduled_task.py b/InvenTree/plugin/samples/integration/scheduled_task.py index 579ad4effe..635bdfe90d 100644 --- a/InvenTree/plugin/samples/integration/scheduled_task.py +++ b/InvenTree/plugin/samples/integration/scheduled_task.py @@ -58,3 +58,4 @@ class ScheduledTaskPlugin(ScheduleMixin, SettingsMixin, IntegrationPluginBase): t_or_f = self.get_setting('T_OR_F') print(f"Called member_func - value is {t_or_f}") + return t_or_f diff --git a/InvenTree/plugin/samples/integration/test_scheduled_task.py b/InvenTree/plugin/samples/integration/test_scheduled_task.py index a43356c36d..4d41eea7c5 100644 --- a/InvenTree/plugin/samples/integration/test_scheduled_task.py +++ b/InvenTree/plugin/samples/integration/test_scheduled_task.py @@ -17,7 +17,7 @@ class ScheduledTaskPluginTests(TestCase): self.assertTrue(plg) # check that the built-in function is running - plg.member_func() + self.assertEqual(plg.member_func(), False) # check that the tasks are defined self.assertEqual(plg.get_task_names(), ['plugin.schedule.member', 'plugin.schedule.hello', 'plugin.schedule.world']) @@ -31,4 +31,4 @@ class ScheduledTaskPluginTests(TestCase): def test_calling(self): """check if a function can be called without errors""" - call_function('schedule', 'member_func') + self.assertEqual(call_function('schedule', 'member_func'), False) From 07a86a3883af719e843c1af1ad9725c0b1323f22 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 May 2022 22:05:56 +0200 Subject: [PATCH 60/85] ognore catches in coverage --- InvenTree/users/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index bda1074601..8f42268224 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -564,14 +564,14 @@ class Owner(models.Model): try: owners.append(cls.objects.get(owner_id=user.pk, owner_type=user_type)) - except: + except: # pragma: no cover pass for group in user.groups.all(): try: owner = cls.objects.get(owner_id=group.pk, owner_type=group_type) owners.append(owner) - except: + except: # pragma: no cover pass return owners From 58a5eac66a222981403c4825034f2c73917a3d9b Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 May 2022 22:06:10 +0200 Subject: [PATCH 61/85] ignore outside sample code in coverage --- InvenTree/plugin/samples/integration/scheduled_task.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugin/samples/integration/scheduled_task.py b/InvenTree/plugin/samples/integration/scheduled_task.py index 635bdfe90d..9ec70e2795 100644 --- a/InvenTree/plugin/samples/integration/scheduled_task.py +++ b/InvenTree/plugin/samples/integration/scheduled_task.py @@ -8,11 +8,11 @@ from plugin.mixins import ScheduleMixin, SettingsMixin # Define some simple tasks to perform def print_hello(): - print("Hello") + print("Hello") # pragma: no cover def print_world(): - print("World") + print("World") # pragma: no cover class ScheduledTaskPlugin(ScheduleMixin, SettingsMixin, IntegrationPluginBase): From 4e5015e7420380ad6a6f58d37281f378a677510e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 May 2022 22:08:57 +0200 Subject: [PATCH 62/85] do not cover unready db --- InvenTree/plugin/builtin/integration/mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index 62ce38a673..d37c8db58d 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -209,7 +209,7 @@ class ScheduleMixin: repeats=task.get('repeats', -1), ) - except (ProgrammingError, OperationalError): + except (ProgrammingError, OperationalError): # pragma: no cover # Database might not yet be ready logger.warning("register_tasks failed, database not ready") From 4663815cfa53799614200ebfcb2e998aa0c2db2e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 May 2022 22:09:20 +0200 Subject: [PATCH 63/85] this is just an extra safety check - should not trigger --- InvenTree/plugin/builtin/integration/mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index d37c8db58d..9d043b7c91 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -171,7 +171,7 @@ class ScheduleMixin: if Schedule.objects.filter(name=task_name).exists(): # Scheduled task already exists - continue! - continue + continue # pragma: no cover logger.info(f"Adding scheduled task '{task_name}'") From 0a012c3c584e19d9de167fa9c348db93b47b5b24 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 May 2022 22:09:58 +0200 Subject: [PATCH 64/85] only possible in test cases with plugin_tests off --- InvenTree/plugin/builtin/integration/mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index 9d043b7c91..d17cebf283 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -56,7 +56,7 @@ class SettingsMixin: if not plugin: # Cannot find associated plugin model, return - return + return # pragma: no cover PluginSetting.set_setting(key, value, user, plugin=plugin) From b9cbc287f24911d026b8103348cb8cfc566c98d1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 May 2022 22:10:26 +0200 Subject: [PATCH 65/85] empty mixin schaffold - no tests --- InvenTree/plugin/builtin/integration/mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index d17cebf283..0e1dd3518e 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -408,7 +408,7 @@ class LabelPrintingMixin: """ MIXIN_NAME = 'Label printing' - def __init__(self): + def __init__(self): # pragma: no cover super().__init__() self.add_mixin('labels', True, __class__) From 08e278232b4b8b3c3d817f29866afcde065516cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 May 2022 22:12:34 +0200 Subject: [PATCH 66/85] ignore db not ready in coverage --- InvenTree/plugin/builtin/integration/mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index 0e1dd3518e..19ee04b77f 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -230,7 +230,7 @@ class ScheduleMixin: scheduled_task.delete() except Schedule.DoesNotExist: pass - except (ProgrammingError, OperationalError): + except (ProgrammingError, OperationalError): # pragma: no cover # Database might not yet be ready logger.warning("unregister_tasks failed, database not ready") From a357c4ef88ab4fc8674b6edb4cdaff307001ce2d Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 May 2022 22:13:29 +0200 Subject: [PATCH 67/85] also cover unregistering --- InvenTree/plugin/samples/integration/test_scheduled_task.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/InvenTree/plugin/samples/integration/test_scheduled_task.py b/InvenTree/plugin/samples/integration/test_scheduled_task.py index 4d41eea7c5..00488bdb6a 100644 --- a/InvenTree/plugin/samples/integration/test_scheduled_task.py +++ b/InvenTree/plugin/samples/integration/test_scheduled_task.py @@ -29,6 +29,11 @@ class ScheduledTaskPluginTests(TestCase): scheduled_plugin_tasks = Schedule.objects.filter(name__istartswith="plugin.") self.assertEqual(len(scheduled_plugin_tasks), 3) + # test unregistering + plg.unregister_tasks() + scheduled_plugin_tasks = Schedule.objects.filter(name__istartswith="plugin.") + self.assertEqual(len(scheduled_plugin_tasks), 0) + def test_calling(self): """check if a function can be called without errors""" self.assertEqual(call_function('schedule', 'member_func'), False) From 626e3838aeea970cad6fd5d8eb9b0349bd631d99 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 May 2022 22:15:24 +0200 Subject: [PATCH 68/85] also cover errors --- InvenTree/plugin/samples/integration/test_scheduled_task.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/InvenTree/plugin/samples/integration/test_scheduled_task.py b/InvenTree/plugin/samples/integration/test_scheduled_task.py index 00488bdb6a..bbacad79d3 100644 --- a/InvenTree/plugin/samples/integration/test_scheduled_task.py +++ b/InvenTree/plugin/samples/integration/test_scheduled_task.py @@ -29,6 +29,11 @@ class ScheduledTaskPluginTests(TestCase): scheduled_plugin_tasks = Schedule.objects.filter(name__istartswith="plugin.") self.assertEqual(len(scheduled_plugin_tasks), 3) + # delete middle task + # this is to check the system also deals with disappearing tasks + scheduled_plugin_tasks[1].delete() + self.assertEqual(len(scheduled_plugin_tasks), 2) + # test unregistering plg.unregister_tasks() scheduled_plugin_tasks = Schedule.objects.filter(name__istartswith="plugin.") From 204b4fd527f1c7a1ebe34f03d2b5282e28987bfb Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 May 2022 22:55:39 +0200 Subject: [PATCH 69/85] rename test --- InvenTree/plugin/samples/integration/test_scheduled_task.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugin/samples/integration/test_scheduled_task.py b/InvenTree/plugin/samples/integration/test_scheduled_task.py index bbacad79d3..d0a9add49f 100644 --- a/InvenTree/plugin/samples/integration/test_scheduled_task.py +++ b/InvenTree/plugin/samples/integration/test_scheduled_task.py @@ -6,8 +6,8 @@ from plugin import registry from plugin.registry import call_function -class ScheduledTaskPluginTests(TestCase): - """ Tests for ScheduledTaskPlugin """ +class ExampleScheduledTaskPluginTests(TestCase): + """ Tests for provided ScheduledTaskPlugin """ def test_function(self): """check if the scheduling works""" From d49f74746aaf03a911516c0696c78576f619829e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 May 2022 22:56:28 +0200 Subject: [PATCH 70/85] test that MixinImplementationErrors raise --- .../integration/test_scheduled_task.py | 110 +++++++++++++++++- 1 file changed, 109 insertions(+), 1 deletion(-) diff --git a/InvenTree/plugin/samples/integration/test_scheduled_task.py b/InvenTree/plugin/samples/integration/test_scheduled_task.py index d0a9add49f..7a3e34ad53 100644 --- a/InvenTree/plugin/samples/integration/test_scheduled_task.py +++ b/InvenTree/plugin/samples/integration/test_scheduled_task.py @@ -2,8 +2,10 @@ from django.test import TestCase -from plugin import registry +from plugin import registry, IntegrationPluginBase +from plugin.helpers import MixinImplementationError from plugin.registry import call_function +from plugin.mixins import ScheduleMixin class ExampleScheduledTaskPluginTests(TestCase): @@ -42,3 +44,109 @@ class ExampleScheduledTaskPluginTests(TestCase): def test_calling(self): """check if a function can be called without errors""" self.assertEqual(call_function('schedule', 'member_func'), False) + + +class ScheduledTaskPluginTests(TestCase): + """ Tests for ScheduledTaskPluginTests mixin base """ + + def test_init(self): + """Check that all MixinImplementationErrors raise""" + class Base(ScheduleMixin, IntegrationPluginBase): + PLUGIN_NAME = 'APlugin' + + class NoSchedules(Base): + """Plugin without schedules""" + pass + + with self.assertRaises(MixinImplementationError): + NoSchedules() + + class WrongFuncSchedules(Base): + """ + Plugin with broken functions + + This plugin is missing a func + """ + + SCHEDULED_TASKS = { + 'test': { + 'schedule': 'I', + 'minutes': 30, + }, + } + + def test(self): + pass + + with self.assertRaises(MixinImplementationError): + WrongFuncSchedules() + + class WrongFuncSchedules1(WrongFuncSchedules): + """ + Plugin with broken functions + + This plugin is missing a schedule + """ + + SCHEDULED_TASKS = { + 'test': { + 'func': 'test', + 'minutes': 30, + }, + } + + with self.assertRaises(MixinImplementationError): + WrongFuncSchedules1() + + + class WrongFuncSchedules2(WrongFuncSchedules): + """ + Plugin with broken functions + + This plugin is missing a schedule + """ + + SCHEDULED_TASKS = { + 'test': { + 'func': 'test', + 'minutes': 30, + }, + } + + with self.assertRaises(MixinImplementationError): + WrongFuncSchedules2() + + class WrongFuncSchedules3(WrongFuncSchedules): + """ + Plugin with broken functions + + This plugin has a broken schedule + """ + + SCHEDULED_TASKS = { + 'test': { + 'func': 'test', + 'schedule': 'XX', + 'minutes': 30, + }, + } + + with self.assertRaises(MixinImplementationError): + WrongFuncSchedules3() + + class WrongFuncSchedules4(WrongFuncSchedules): + """ + Plugin with broken functions + + This plugin is missing a minute marker for its schedule + """ + + SCHEDULED_TASKS = { + 'test': { + 'func': 'test', + 'schedule': 'I', + }, + } + + with self.assertRaises(MixinImplementationError): + WrongFuncSchedules4() From 096c0c876cc99450a6b954cae20aa183d48bcd90 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 May 2022 22:58:24 +0200 Subject: [PATCH 71/85] PEP fix --- .../plugin/samples/integration/test_scheduled_task.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/InvenTree/plugin/samples/integration/test_scheduled_task.py b/InvenTree/plugin/samples/integration/test_scheduled_task.py index 7a3e34ad53..1909c9a80e 100644 --- a/InvenTree/plugin/samples/integration/test_scheduled_task.py +++ b/InvenTree/plugin/samples/integration/test_scheduled_task.py @@ -64,7 +64,7 @@ class ScheduledTaskPluginTests(TestCase): class WrongFuncSchedules(Base): """ Plugin with broken functions - + This plugin is missing a func """ @@ -84,7 +84,7 @@ class ScheduledTaskPluginTests(TestCase): class WrongFuncSchedules1(WrongFuncSchedules): """ Plugin with broken functions - + This plugin is missing a schedule """ @@ -98,11 +98,10 @@ class ScheduledTaskPluginTests(TestCase): with self.assertRaises(MixinImplementationError): WrongFuncSchedules1() - class WrongFuncSchedules2(WrongFuncSchedules): """ Plugin with broken functions - + This plugin is missing a schedule """ @@ -119,7 +118,7 @@ class ScheduledTaskPluginTests(TestCase): class WrongFuncSchedules3(WrongFuncSchedules): """ Plugin with broken functions - + This plugin has a broken schedule """ @@ -137,7 +136,7 @@ class ScheduledTaskPluginTests(TestCase): class WrongFuncSchedules4(WrongFuncSchedules): """ Plugin with broken functions - + This plugin is missing a minute marker for its schedule """ From 1522d330fa8a341867fb7cc6a186ccb248f694e1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 May 2022 23:06:11 +0200 Subject: [PATCH 72/85] add test for is_sample --- InvenTree/plugin/test_integration.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/InvenTree/plugin/test_integration.py b/InvenTree/plugin/test_integration.py index 3e4c38f968..5b211e2d96 100644 --- a/InvenTree/plugin/test_integration.py +++ b/InvenTree/plugin/test_integration.py @@ -11,6 +11,8 @@ from plugin import IntegrationPluginBase from plugin.mixins import AppMixin, SettingsMixin, UrlsMixin, NavigationMixin, APICallMixin from plugin.urls import PLUGIN_BASE +from plugin.samples.integration.sample import SampleIntegrationPlugin + class BaseMixinDefinition: def test_mixin_name(self): @@ -238,6 +240,7 @@ class IntegrationPluginBaseTests(TestCase): LICENSE = 'MIT' self.plugin_name = NameIntegrationPluginBase() + self.plugin_sample = SampleIntegrationPlugin() def test_action_name(self): """check the name definition possibilities""" @@ -246,6 +249,10 @@ class IntegrationPluginBaseTests(TestCase): self.assertEqual(self.plugin_simple.plugin_name(), 'SimplePlugin') self.assertEqual(self.plugin_name.plugin_name(), 'Aplugin') + # is_sampe + self.assertEqual(self.plugin.is_sample, False) + self.assertEqual(self.plugin_sample.is_sample, True) + # slug self.assertEqual(self.plugin.slug, '') self.assertEqual(self.plugin_simple.slug, 'simpleplugin') From e95c3e09489c5ab6541dfadd656e0954d98951e1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 May 2022 23:06:38 +0200 Subject: [PATCH 73/85] do not cover default returns --- InvenTree/plugin/helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py index 7f9f2be740..05bd44d4f3 100644 --- a/InvenTree/plugin/helpers.py +++ b/InvenTree/plugin/helpers.py @@ -75,7 +75,7 @@ def handle_error(error, do_raise: bool = True, do_log: bool = True, log_name: st path_parts.remove('plugin') path_parts.pop(0) else: - path_parts.remove('plugins') + path_parts.remove('plugins') # pragma: no cover package_name = '.'.join(path_parts) @@ -135,7 +135,7 @@ def check_git_version(): except ValueError: # pragma: no cover pass - return False + return False # pragma: no cover class GitStatus: From c5acfaf541e5ae6dc89cdbcae5bad3f3f37fe183 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 May 2022 23:06:57 +0200 Subject: [PATCH 74/85] do not coverage catch for load --- InvenTree/plugin/apps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/apps.py b/InvenTree/plugin/apps.py index 8de4cb9b6c..521d42b743 100644 --- a/InvenTree/plugin/apps.py +++ b/InvenTree/plugin/apps.py @@ -35,7 +35,7 @@ class PluginAppConfig(AppConfig): if InvenTreeSetting.get_setting('PLUGIN_ON_STARTUP', create=False): # make sure all plugins are installed registry.install_plugin_file() - except: + except: # pragma: no cover pass # get plugins and init them From 5e277130fa57a4362e60b016b597f22d1ca2aabc Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 May 2022 23:07:16 +0200 Subject: [PATCH 75/85] do not cover not implemented Mixin --- InvenTree/plugin/builtin/integration/mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index 19ee04b77f..ebe3ebf553 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -426,7 +426,7 @@ class LabelPrintingMixin: """ # Unimplemented (to be implemented by the particular plugin class) - ... + ... # pragma: no cover class APICallMixin: From cf0cbff69af6ba6508a30e42632ff9d291f26b09 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 May 2022 23:19:01 +0200 Subject: [PATCH 76/85] we are not covering packages right now --- InvenTree/plugin/integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/integration.py b/InvenTree/plugin/integration.py index cab3e81a8b..c622c0402c 100644 --- a/InvenTree/plugin/integration.py +++ b/InvenTree/plugin/integration.py @@ -191,7 +191,7 @@ class IntegrationPluginBase(MixinBase, plugin_base.InvenTreePluginBase): Path to the plugin """ if self._is_package: - return self.__module__ + return self.__module__ # pragma: no cover return pathlib.Path(self.def_path).relative_to(settings.BASE_DIR) @property From e1b395c9be802f3148c2060d4bb494595aed7441 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 May 2022 23:19:51 +0200 Subject: [PATCH 77/85] test is_active --- InvenTree/plugin/test_plugin.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/InvenTree/plugin/test_plugin.py b/InvenTree/plugin/test_plugin.py index f88b6e6176..c0835c2fb3 100644 --- a/InvenTree/plugin/test_plugin.py +++ b/InvenTree/plugin/test_plugin.py @@ -31,6 +31,10 @@ class InvenTreePluginTests(TestCase): self.assertEqual(self.named_plugin.PLUGIN_NAME, 'abc123') self.assertEqual(self.named_plugin.plugin_name(), 'abc123') + def test_basic_is_active(self): + """check if a basic plugin is active""" + self.assertEqual(self.plugin.is_active(), False) + class PluginTagTests(TestCase): """ Tests for the plugin extras """ From 6504ef535a2b3f716ba2cdcb46eba165a5d544fb Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 May 2022 23:23:36 +0200 Subject: [PATCH 78/85] fix test - len needs to be recalculated --- InvenTree/plugin/samples/integration/test_scheduled_task.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/InvenTree/plugin/samples/integration/test_scheduled_task.py b/InvenTree/plugin/samples/integration/test_scheduled_task.py index 1909c9a80e..1bcd2015f1 100644 --- a/InvenTree/plugin/samples/integration/test_scheduled_task.py +++ b/InvenTree/plugin/samples/integration/test_scheduled_task.py @@ -34,6 +34,8 @@ class ExampleScheduledTaskPluginTests(TestCase): # delete middle task # this is to check the system also deals with disappearing tasks scheduled_plugin_tasks[1].delete() + # there should be one less now + scheduled_plugin_tasks = Schedule.objects.filter(name__istartswith="plugin.") self.assertEqual(len(scheduled_plugin_tasks), 2) # test unregistering From 8038cff87481f621b5d8267b89a15cd81567963c Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 4 May 2022 00:43:49 +0200 Subject: [PATCH 79/85] ignore for coverage --- InvenTree/plugin/helpers.py | 2 +- InvenTree/plugin/registry.py | 6 +++--- InvenTree/plugin/samples/integration/test_scheduled_task.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py index 05bd44d4f3..f1753b1b45 100644 --- a/InvenTree/plugin/helpers.py +++ b/InvenTree/plugin/helpers.py @@ -88,7 +88,7 @@ def handle_error(error, do_raise: bool = True, do_log: bool = True, log_name: st if do_raise: # do a straight raise if we are playing with enviroment variables at execution time, ignore the broken sample if settings.TESTING_ENV and package_name != 'integration.broken_sample' and isinstance(error, IntegrityError): - raise error + raise error # pragma: no cover raise IntegrationPluginError(package_name, str(error)) # endregion diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index 304932f6f8..240bd3446b 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -283,7 +283,7 @@ class PluginsRegistry: if not settings.PLUGIN_TESTING: raise error # pragma: no cover plugin_db_setting = None - except (IntegrityError) as error: + except (IntegrityError) as error: # pragma: no cover logger.error(f"Error initializing plugin: {error}") # Always activate if testing @@ -322,7 +322,7 @@ class PluginsRegistry: self.plugins[plugin.slug] = plugin else: # save for later reference - self.plugins_inactive[plug_key] = plugin_db_setting + self.plugins_inactive[plug_key] = plugin_db_setting # pragma: no cover def _activate_plugins(self, force_reload=False): """ @@ -411,7 +411,7 @@ class PluginsRegistry: deleted_count += 1 if deleted_count > 0: - logger.info(f"Removed {deleted_count} old scheduled tasks") + logger.info(f"Removed {deleted_count} old scheduled tasks") # pragma: no cover except (ProgrammingError, OperationalError): # Database might not yet be ready logger.warning("activate_integration_schedule failed, database not ready") diff --git a/InvenTree/plugin/samples/integration/test_scheduled_task.py b/InvenTree/plugin/samples/integration/test_scheduled_task.py index 1bcd2015f1..314f3f3f1f 100644 --- a/InvenTree/plugin/samples/integration/test_scheduled_task.py +++ b/InvenTree/plugin/samples/integration/test_scheduled_task.py @@ -78,7 +78,7 @@ class ScheduledTaskPluginTests(TestCase): } def test(self): - pass + pass # pragma: no cover with self.assertRaises(MixinImplementationError): WrongFuncSchedules() From 75e24635b53cf6cc08cf3455ba081128423065a4 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 4 May 2022 09:17:42 +1000 Subject: [PATCH 80/85] Fix speling errors in translated strings --- InvenTree/InvenTree/helpers.py | 2 +- InvenTree/build/models.py | 2 +- InvenTree/build/serializers.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index d9dfaa395d..36cd288232 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -537,7 +537,7 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int): # The number of extracted serial numbers must match the expected quantity if not expected_quantity == len(numbers): - raise ValidationError([_("Number of unique serial number ({s}) must match quantity ({q})").format(s=len(numbers), q=expected_quantity)]) + raise ValidationError([_("Number of unique serial numbers ({s}) must match quantity ({q})").format(s=len(numbers), q=expected_quantity)]) return numbers diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 1fdf613b68..43bca0e238 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -1177,7 +1177,7 @@ class BuildItem(models.Model): a = normalize(self.stock_item.quantity) raise ValidationError({ - 'quantity': _(f'Allocated quantity ({q}) must not execed available stock quantity ({a})') + 'quantity': _(f'Allocated quantity ({q}) must not exceed available stock quantity ({a})') }) # Allocated quantity cannot cause the stock item to be over-allocated diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 07a0bcc29a..bed4b59203 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -387,7 +387,7 @@ class BuildOutputCompleteSerializer(serializers.Serializer): default=False, required=False, label=_('Accept Incomplete Allocation'), - help_text=_('Complete ouputs if stock has not been fully allocated'), + help_text=_('Complete outputs if stock has not been fully allocated'), ) notes = serializers.CharField( From dbc0023c57d5d2fa0522d8674aa2aa66958be980 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 4 May 2022 12:10:00 +1000 Subject: [PATCH 81/85] Update CONTRIBUTING.md Update labels a bit --- CONTRIBUTING.md | 65 +++++++++++++++++++++++-------------------------- 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 145f50aa69..c36c11b62b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -131,37 +131,34 @@ HTML and javascript files are passed through the django templating engine. Trans The tags describe issues and PRs in multiple areas: | Area | Name | Description | |---|---|---| -| Feature | | | -| | feat: API | tbd | -| | feat: barcode | tbd | -| | feat: build | tbd | -| | feat: docker | tbd | -| | feat: importer | tbd | -| | feat: order | tbd | -| | feat: part | tbd | -| | feat: plugin | tbd | -| | feat: pricing | tbd | -| | feat: report | tbd | -| | feat: setup | tbd | -| | feat: stock | tbd | -| | feat: user interface | tbd | -| Type | | | -| | typ: bug | tbd | -| | typ: dependencies | tbd | -| | typ: enhancement | tbd | -| | typ: security | tbd | -| | typ: question | tbd | -| | typ: roadmap | tbd | -| State | | | -| | state: duplicate | tbd | -| | state: invalid | tbd | -| | state: no-activity | tbd | -| | state: duplicate | tbd | -| | state: wontfix | tbd | -| Ecosystem | | | -| | eco: app | tbd | -| | eco: CI | tbd | -| | eco: demo | tbd | -| GH Built in | | | -| | help wanted | tbd | -| | starter | tbd | +| Type Labels | | | +| | bug | Identifies a bug which needs to be addressed | +| | dependency | Relates to a project dependency | +| | duplicate | Duplicate of another issue or PR | +| | enhancement | This is an suggested enhancement or new feature | +| | help wanted | Assistance required | +| | invalid | This issue or PR is considered invalid | +| | inactive | Indicates lack of activity | +| | question | This is a question | +| | roadmap | This is a roadmap feature with no immediate plans for implementation | +| | security | Relates to a security issue | +| | starter | Good issue for a developer new to the project | +| | wontfix | No work will be done against this issue or PR | +| Feature Labels | | | +| | API | Relates to the API | +| | barcode | Barcode scanning and integration | +| | build | Build orders | +| | importer | Data importing and processing | +| | order | Purchase order and sales orders | +| | part | Parts | +| | plugin | Plugin ecosystem | +| | pricing | Pricing functionality | +| | report | Report generation | +| | stock | Stock item management | +| | user interface | User interface | +| Ecosystem Labels | | | +| | demo | Relates to the InvenTree demo server or dataset | +| | docker | Docker / docker-compose | +| | CI | CI / unit testing ecosystem | +| | setup | Relates to the InvenTree setup / installation process | + From 00dffd953be5913f3cfaf3c5fe3ae181e4bbb5c8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 4 May 2022 12:39:12 +0200 Subject: [PATCH 82/85] add messages if company was deleted --- InvenTree/company/templates/company/manufacturer_part.html | 3 +++ InvenTree/company/templates/company/supplier_part.html | 3 +++ InvenTree/order/templates/order/order_base.html | 3 +++ InvenTree/report/templates/report/inventree_po_report.html | 2 +- 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/InvenTree/company/templates/company/manufacturer_part.html b/InvenTree/company/templates/company/manufacturer_part.html index 881e5870ca..e3bcb3dd7e 100644 --- a/InvenTree/company/templates/company/manufacturer_part.html +++ b/InvenTree/company/templates/company/manufacturer_part.html @@ -91,6 +91,9 @@ src="{% static 'img/blank_image.png' %}" {% if part.manufacturer %} {{ part.manufacturer.name }}{% include "clip.html"%} + {% else %} + {% trans "No manufacturer information available" %} + {% endif %} {% endif %} diff --git a/InvenTree/company/templates/company/supplier_part.html b/InvenTree/company/templates/company/supplier_part.html index 5471da15cc..930d8260e1 100644 --- a/InvenTree/company/templates/company/supplier_part.html +++ b/InvenTree/company/templates/company/supplier_part.html @@ -87,6 +87,9 @@ src="{% static 'img/blank_image.png' %}" {% trans "Supplier" %} {% if part.supplier %} {{ part.supplier.name }}{% include "clip.html"%} + {% else %} + {% trans "No supplier information available" %} + {% endif %} {% endif %} diff --git a/InvenTree/order/templates/order/order_base.html b/InvenTree/order/templates/order/order_base.html index 7596e8da3a..5302b57e09 100644 --- a/InvenTree/order/templates/order/order_base.html +++ b/InvenTree/order/templates/order/order_base.html @@ -113,6 +113,9 @@ src="{% static 'img/blank_image.png' %}" {% if order.supplier %} {{ order.supplier.name }}{% include "clip.html"%} + {% else %} + {% trans "No suppplier information available" %} + {% endif %} {% endif %} diff --git a/InvenTree/report/templates/report/inventree_po_report.html b/InvenTree/report/templates/report/inventree_po_report.html index 427410576c..9e546fb70e 100644 --- a/InvenTree/report/templates/report/inventree_po_report.html +++ b/InvenTree/report/templates/report/inventree_po_report.html @@ -74,7 +74,7 @@ table td.expand {

    {% trans "Purchase Order" %} {{ prefix }}{{ reference }}

    - {% if supplier %}{{ supplier.name }}{% endif %} + {% if supplier %}{{ supplier.name }}{% endif %}{% else %}{% trans "Supplier was deleted" %}{% endif %}
    {% endblock %} From 5435cd28c9de8b3a45393d6fd9f09b5ffe8d274e Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 4 May 2022 12:39:56 +0200 Subject: [PATCH 83/85] redirect to index if company was deleted --- InvenTree/company/templates/company/manufacturer_part.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/InvenTree/company/templates/company/manufacturer_part.html b/InvenTree/company/templates/company/manufacturer_part.html index e3bcb3dd7e..5a0e741c1a 100644 --- a/InvenTree/company/templates/company/manufacturer_part.html +++ b/InvenTree/company/templates/company/manufacturer_part.html @@ -338,6 +338,8 @@ $('#delete-part').click(function() { onSuccess: function() { {% if part.manufacturer %} window.location.href = "{% url 'company-detail' part.manufacturer.id %}"; + {% else%} + window.location.href = "{% url 'index' %}"; {% endif %} } }); From 19d3b03280c2064bf888a13db783a96d91acda6d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 4 May 2022 13:10:49 +0200 Subject: [PATCH 84/85] fix double endif --- InvenTree/order/templates/order/order_base.html | 1 - 1 file changed, 1 deletion(-) diff --git a/InvenTree/order/templates/order/order_base.html b/InvenTree/order/templates/order/order_base.html index 5302b57e09..b80275b1f3 100644 --- a/InvenTree/order/templates/order/order_base.html +++ b/InvenTree/order/templates/order/order_base.html @@ -116,7 +116,6 @@ src="{% static 'img/blank_image.png' %}" {% else %} {% trans "No suppplier information available" %} {% endif %} - {% endif %} {% if order.supplier_reference %} From 055b9c9a463ab2baf4099d234da4d8a95bcdbc08 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 4 May 2022 13:57:35 +0200 Subject: [PATCH 85/85] remove duplicate endif --- InvenTree/company/templates/company/supplier_part.html | 1 - 1 file changed, 1 deletion(-) diff --git a/InvenTree/company/templates/company/supplier_part.html b/InvenTree/company/templates/company/supplier_part.html index 930d8260e1..f990b66898 100644 --- a/InvenTree/company/templates/company/supplier_part.html +++ b/InvenTree/company/templates/company/supplier_part.html @@ -90,7 +90,6 @@ src="{% static 'img/blank_image.png' %}" {% else %} {% trans "No supplier information available" %} {% endif %} - {% endif %}