From b4f8136142aa0901ed9da32bfe9dd28c8193f3b8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 21 Mar 2022 21:58:21 +1100 Subject: [PATCH 01/58] 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/58] 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/58] 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/58] 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 e1fbd961e50706a4e884392b0101caf4352eb428 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 7 Apr 2022 19:05:25 +1000 Subject: [PATCH 05/58] 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 06/58] 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 07/58] 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 08/58] 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 09/58] 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 10/58] 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 9022bd327087b953b7415034c55d49b1e1ec4878 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 2 May 2022 01:46:06 +0200 Subject: [PATCH 11/58] Add new readme --- README.md | 192 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 145 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index dd76fd41dd..9eb6229d46 100644 --- a/README.md +++ b/README.md @@ -1,78 +1,176 @@ +
+ InvenTree logo +

InvenTree

+

Open Source Inventory Management System

-InvenTree - -# InvenTree - -

- follow on Twitter

- -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Coverage Status](https://coveralls.io/repos/github/inventree/InvenTree/badge.svg)](https://coveralls.io/github/inventree/InvenTree) -[![Crowdin](https://badges.crowdin.net/inventree/localized.svg)](https://crowdin.com/project/inventree) + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/inventree/inventree) ![CI](https://github.com/inventree/inventree/actions/workflows/qc_checks.yaml/badge.svg) +![Docker Build](https://github.com/inventree/inventree/actions/workflows/docker_latest.yaml/badge.svg) + +![Coveralls](https://img.shields.io/coveralls/github/inventree/InvenTree) +[![Crowdin](https://badges.crowdin.net/inventree/localized.svg)](https://crowdin.com/project/inventree) +![Lines of code](https://img.shields.io/tokei/lines/github/inventree/InvenTree) +![GitHub commit activity](https://img.shields.io/github/commit-activity/m/inventree/inventree) +![PyPI - Downloads](https://img.shields.io/pypi/dm/inventree) +[![Docker Pulls](https://img.shields.io/docker/pulls/inventree/inventree)](https://hub.docker.com/r/inventree/inventree) + +![GitHub Org's stars](https://img.shields.io/github/stars/inventree?style=social) +![Twitter Follow](https://img.shields.io/twitter/follow/inventreedb?style=social) +![Subreddit subscribers](https://img.shields.io/reddit/subreddit-subscribers/inventree?style=social) + + +

+ View Demo + · + Documentation + · + Report Bug + · + Request Feature +

+
+ + +## :star2: About the Project InvenTree is an open-source Inventory Management System which provides powerful low-level stock control and part tracking. The core of the InvenTree system is a Python/Django database backend which provides an admin interface (web-based) and a REST API for interaction with external interfaces and applications. +
+ screenshot +
+ InvenTree is designed to be lightweight and easy to use for SME or hobbyist applications, where many existing stock management solutions are bloated and cumbersome to use. Updating stock is a single-action process and does not require a complex system of work orders or stock transactions. Powerful business logic works in the background to ensure that stock tracking history is maintained, and users have ready access to stock level information. -# Demo + -A demo instance of InvenTree is provided to allow users to explore the functionality of the software. [Read more here](https://inventree.readthedocs.io/en/latest/demo/) +### :dart: Features -# Docker +- Organize Parts and BOMs +- Manage Suppliers +- Instant Stock Knowledge +- Extend and Customize to fit your usage -[![Docker Pulls](https://img.shields.io/docker/pulls/inventree/inventree)](https://hub.docker.com/r/inventree/inventree) -![Docker Build](https://github.com/inventree/inventree/actions/workflows/docker_latest.yaml/badge.svg) + +### :compass: Roadmap -InvenTree is [available via Docker](https://hub.docker.com/r/inventree/inventree). Read the [docker guide](https://inventree.readthedocs.io/en/latest/start/docker/) for full details. +* [x] Plugins +* [ ] Improved Importers +* [ ] Custom Workflow Processes -# Mobile App + +### :hammer_and_wrench: Integration -InvenTree is supported by a [companion mobile app](https://inventree.readthedocs.io/en/latest/app/app/) which allows users access to stock control information and functionality. - -- [**Download InvenTree from the Android Play Store**](https://play.google.com/store/apps/details?id=inventree.inventree_app) - -- [**Download InvenTree from the Apple App Store**](https://apps.apple.com/au/app/inventree/id1581731101#?platform=iphone) - -# Deploy to DigitalOcean -[![Deploy to DO](https://www.deploytodo.com/do-btn-blue-ghost.svg)](https://marketplace.digitalocean.com/apps/inventree?refcode=d6172576d014) - -# Documentation - -For InvenTree documentation, refer to the [InvenTree documentation website](https://inventree.readthedocs.io/en/latest/). - -# Getting Started - -Refer to the [getting started guide](https://inventree.readthedocs.io/en/latest/start/install/) for installation and setup instructions. - -# Credits - -The credits for all used packages are part of the [InvenTree documentation website](https://inventree.readthedocs.io/en/latest/credits/). - -# Integration - -InvenTree is designed to be extensible, and provides multiple options for integration with external applications or addition of custom plugins: +InvenTree is designed to be **extensible**, and provides multiple options for **integration** with external applications or addition of custom plugins: * [InvenTree API](https://inventree.readthedocs.io/en/latest/extend/api/) * [Python module](https://inventree.readthedocs.io/en/latest/extend/python) * [Plugin interface](https://inventree.readthedocs.io/en/latest/extend/plugins) -* [Third party](https://inventree.readthedocs.io/en/latest/extend/integrate) +* [Third party tools](https://inventree.readthedocs.io/en/latest/extend/integrate) -# Contributing + +### :space_invader: Tech Stack -Contributions are welcomed and encouraged. Please help to make this project even better! Refer to the [contribution page](https://inventree.readthedocs.io/en/latest/contribute/). +
+ Server + +
-# Translation +
+Database + +
+ +
+ Client + +
+ +
+DevOps + +
+ + +## :toolbox: Getting Started + +Refer to the [getting started guide](https://inventree.readthedocs.io/en/latest/start/install/) for installation and setup instructions. + + +## :iphone: Mobile App + +InvenTree is supported by a [companion mobile app](https://inventree.readthedocs.io/en/latest/app/app/) which allows users access to stock control information and functionality. + + + + +## :train: Deploy + +There are several options to deploy InvenTree. + +

+ Docker + · + Deploy to DO + · + Bare Metal +

+ + +## :wave: Contributing + +Contributions are welcomed and encouraged. Please help to make this project even better! +Refer to the [contribution page in the docs ](https://inventree.readthedocs.io/en/latest/contribute/) and check out [contributing.md](CONTRIBUTING.md). + + +## :scroll: Translation Native language translation of the InvenTree web application is [community contributed via crowdin](https://crowdin.com/project/inventree). **Contributions are welcomed and encouraged**. To contribute to the translation effort, navigate to the [InvenTree crowdin project](https://crowdin.com/project/inventree), create a free account, and start making translations suggestions for your language of choice! -# Donate + +## :money_with_wings: Sponsor If you use InvenTree and find it to be useful, please consider making a donation toward its continued development. [Donate via PayPal](https://paypal.me/inventree?locale.x=en_AU) + + +## :gem: Acknowledgements + +We would like to acknowledge a few special projects: + - [PartKeepr](https://github.com/partkeepr/PartKeepr) as a valuable predecessor and inspiration + - [Readme Template](https://github.com/Louis3797/awesome-readme-template) for the template of this page + +Find a full list of used third-party libraries in [our documentation](https://inventree.readthedocs.io/en/latest/credits/). + + +## :warning: License + +Distributed under the [MIT](https://choosealicense.com/licenses/mit/) License. See LICENSE.txt for more information. From 33a7d98dff5bef4ef29887a554fad1a97504408f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 2 May 2022 21:12:11 +0200 Subject: [PATCH 12/58] remove screenshot it is difficult to find the single thing that descirbes InvenTree --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 9eb6229d46..a499ce8856 100644 --- a/README.md +++ b/README.md @@ -36,10 +36,6 @@ InvenTree is an open-source Inventory Management System which provides powerful low-level stock control and part tracking. The core of the InvenTree system is a Python/Django database backend which provides an admin interface (web-based) and a REST API for interaction with external interfaces and applications. -
- screenshot -
- InvenTree is designed to be lightweight and easy to use for SME or hobbyist applications, where many existing stock management solutions are bloated and cumbersome to use. Updating stock is a single-action process and does not require a complex system of work orders or stock transactions. Powerful business logic works in the background to ensure that stock tracking history is maintained, and users have ready access to stock level information. From f8a2811d9046bd0239ccab57e7b22c95513cd174 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 May 2022 00:55:29 +0200 Subject: [PATCH 13/58] 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 14/58] 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 15/58] 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 16/58] 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 17/58] 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 18/58] 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 19/58] 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 20/58] 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 21/58] 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 22/58] 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 23/58] 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 24/58] 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 25/58] 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 26/58] 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 27/58] 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 28/58] 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 29/58] 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 30/58] 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 31/58] 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 32/58] 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 33/58] 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 34/58] 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 35/58] 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 36/58] 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 37/58] 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 38/58] 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 39/58] 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 40/58] 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 41/58] 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 42/58] 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 43/58] 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 44/58] 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 45/58] 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 46/58] 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 47/58] 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 48/58] 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 49/58] 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 50/58] 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 51/58] 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 52/58] 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 53/58] 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 54/58] 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 55/58] 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 56/58] 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 57/58] 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 58/58] 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 | +