From 4a843908ec9b74e076e860ea96d636e95d6c8749 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Wed, 27 Apr 2022 21:56:04 +1000 Subject: [PATCH 01/47] Add customize option to hide admin link in user menu --- InvenTree/InvenTree/settings.py | 2 +- InvenTree/config_template.yaml | 1 + InvenTree/templates/navbar.html | 5 +++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 6e88fe8375..1446c33aad 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -915,7 +915,7 @@ PLUGIN_TESTING_SETUP = get_setting('PLUGIN_TESTING_SETUP', False) # load plugin PLUGIN_RETRY = get_setting('PLUGIN_RETRY', 5) # how often should plugin loading be tried? PLUGIN_FILE_CHECKED = False # Was the plugin file checked? -# user interface customization values +# User interface customization values CUSTOMIZE = get_setting( 'INVENTREE_CUSTOMIZE', CONFIG.get('customize', {}), diff --git a/InvenTree/config_template.yaml b/InvenTree/config_template.yaml index b9d14c4d4b..c60a58d37f 100644 --- a/InvenTree/config_template.yaml +++ b/InvenTree/config_template.yaml @@ -193,3 +193,4 @@ static_root: '/home/inventree/data/static' # login_message: InvenTree demo instance - <a href='https://inventree.readthedocs.io/en/latest/demo/'> Click here for login details</a> # navbar_message: <h6>InvenTree demo mode <a href='https://inventree.readthedocs.io/en/latest/demo/'><span class='fas fa-info-circle'></span></a></h6> # logo: logo.png +# hide_admin_link: true diff --git a/InvenTree/templates/navbar.html b/InvenTree/templates/navbar.html index d7d70db59f..9b5ddd89a7 100644 --- a/InvenTree/templates/navbar.html +++ b/InvenTree/templates/navbar.html @@ -6,9 +6,10 @@ {% settings_value 'BARCODE_ENABLE' as barcodes %} {% settings_value 'STICKY_HEADER' user=request.user as sticky %} {% navigation_enabled as plugin_nav %} -{% inventree_demo_mode as demo %} + {% inventree_show_about user as show_about %} {% inventree_customize 'navbar_message' as navbar_message %} +{% inventree_customize 'hide_admin_link' as hide_admin_link %} <nav class="navbar {% if sticky %}fixed-top{% endif %} navbar-expand-lg navbar-light"> <div class="container-fluid"> @@ -132,7 +133,7 @@ </a> <ul class='dropdown-menu dropdown-menu-end inventree-navbar-menu'> {% if user.is_authenticated %} - {% if user.is_staff and not demo %} + {% if user.is_staff and not hide_admin_link %} <li><a class='dropdown-item' href="{% url 'admin:index' %}"><span class="fas fa-user-shield"></span> {% trans "Admin" %}</a></li> {% endif %} <li><a class='dropdown-item' href="{% url 'settings' %}"><span class="fas fa-cog"></span> {% trans "Settings" %}</a></li> From fc4de6c7b8962a42a93f2887743eb1d30376c56a Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Wed, 27 Apr 2022 22:00:58 +1000 Subject: [PATCH 02/47] Remove other demo references --- InvenTree/InvenTree/settings.py | 9 --------- InvenTree/part/templatetags/inventree_extras.py | 7 ------- InvenTree/templates/InvenTree/settings/user.html | 4 ++-- InvenTree/templates/account/login.html | 3 +-- InvenTree/templates/base.html | 3 +-- 5 files changed, 4 insertions(+), 22 deletions(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 1446c33aad..8f7b9e2e35 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -62,12 +62,6 @@ DEBUG = _is_true(get_setting( CONFIG.get('debug', True) )) -# Determine if we are running in "demo mode" -DEMO_MODE = _is_true(get_setting( - 'INVENTREE_DEMO', - CONFIG.get('demo', False) -)) - DOCKER = _is_true(get_setting( 'INVENTREE_DOCKER', False @@ -217,9 +211,6 @@ MEDIA_URL = '/media/' if DEBUG: logger.info("InvenTree running with DEBUG enabled") -if DEMO_MODE: - logger.warning("InvenTree running in DEMO mode") # pragma: no cover - logger.debug(f"MEDIA_ROOT: '{MEDIA_ROOT}'") logger.debug(f"STATIC_ROOT: '{STATIC_ROOT}'") diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py index 5701658087..889946ff19 100644 --- a/InvenTree/part/templatetags/inventree_extras.py +++ b/InvenTree/part/templatetags/inventree_extras.py @@ -160,13 +160,6 @@ def inventree_in_debug_mode(*args, **kwargs): return djangosettings.DEBUG -@register.simple_tag() -def inventree_demo_mode(*args, **kwargs): - """ Return True if the server is running in DEMO mode """ - - return djangosettings.DEMO_MODE - - @register.simple_tag() def inventree_show_about(user, *args, **kwargs): """ Return True if the about modal should be shown """ diff --git a/InvenTree/templates/InvenTree/settings/user.html b/InvenTree/templates/InvenTree/settings/user.html index 32bc4d43e7..d06df06d1e 100644 --- a/InvenTree/templates/InvenTree/settings/user.html +++ b/InvenTree/templates/InvenTree/settings/user.html @@ -13,8 +13,8 @@ {% endblock %} {% block actions %} -{% inventree_demo_mode as demo %} -{% if not demo %} +{% inventree_customize 'hide_password_reset' as hide_password_reset %} +{% if hide_password_reset %} <div class='btn btn-outline-primary' type='button' id='edit-password' title='{% trans "Change Password" %}'> <span class='fas fa-key'></span> {% trans "Set Password" %} </div> diff --git a/InvenTree/templates/account/login.html b/InvenTree/templates/account/login.html index fcdd08a23c..3e0865c4b9 100644 --- a/InvenTree/templates/account/login.html +++ b/InvenTree/templates/account/login.html @@ -12,7 +12,6 @@ {% settings_value 'LOGIN_ENABLE_SSO' as enable_sso %} {% inventree_customize 'login_message' as login_message %} {% mail_configured as mail_conf %} -{% inventree_demo_mode as demo %} <h1>{% trans "Sign In" %}</h1> @@ -42,7 +41,7 @@ for a account and sign in below:{% endblocktrans %}</p> <div class="btn-group float-right" role="group"> <button class="btn btn-success" type="submit">{% trans "Sign In" %}</button> </div> - {% if mail_conf and enable_pwd_forgot and not demo %} + {% if mail_conf and enable_pwd_forgot %} <a class="" href="{% url 'account_reset_password' %}"><small>{% trans "Forgot Password?" %}</small></a> {% endif %} </form> diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index 0188ecefa5..8c2a949353 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -8,7 +8,6 @@ {% settings_value "SERVER_RESTART_REQUIRED" as server_restart_required %} {% settings_value "LABEL_ENABLE" with user=user as labels_enabled %} {% inventree_show_about user as show_about %} -{% inventree_demo_mode as demo_mode %} <!DOCTYPE html> <html lang="en"> @@ -94,7 +93,7 @@ {% block alerts %} <div class='notification-area' id='alerts'> <!-- Div for displayed alerts --> - {% if server_restart_required and not demo_mode %} + {% if server_restart_required %} <div id='alert-restart-server' class='alert alert-danger' role='alert'> <span class='fas fa-server'></span> <strong>{% trans "Server Restart Required" %}</strong> From 06da120ef33b2682d02dd662946f2ee5b194d8a3 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Wed, 27 Apr 2022 22:03:55 +1000 Subject: [PATCH 03/47] Hide entry box for changing password --- InvenTree/InvenTree/forms.py | 4 ++-- InvenTree/templates/InvenTree/settings/user.html | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/InvenTree/InvenTree/forms.py b/InvenTree/InvenTree/forms.py index 02b993d31b..91863f04e2 100644 --- a/InvenTree/InvenTree/forms.py +++ b/InvenTree/InvenTree/forms.py @@ -150,13 +150,13 @@ class DeleteForm(forms.Form): class EditUserForm(HelperForm): - """ Form for editing user information + """ + Form for editing user information """ class Meta: model = User fields = [ - 'username', 'first_name', 'last_name', ] diff --git a/InvenTree/templates/InvenTree/settings/user.html b/InvenTree/templates/InvenTree/settings/user.html index d06df06d1e..d3d1e35210 100644 --- a/InvenTree/templates/InvenTree/settings/user.html +++ b/InvenTree/templates/InvenTree/settings/user.html @@ -14,14 +14,14 @@ {% block actions %} {% inventree_customize 'hide_password_reset' as hide_password_reset %} -{% if hide_password_reset %} +{% if not hide_password_reset %} <div class='btn btn-outline-primary' type='button' id='edit-password' title='{% trans "Change Password" %}'> <span class='fas fa-key'></span> {% trans "Set Password" %} </div> +{% endif %} <div class='btn btn-primary' type='button' id='edit-user' title='{% trans "Edit User Information" %}'> <span class='fas fa-user-cog'></span> {% trans "Edit" %} </div> -{% endif %} {% endblock %} {% block content %} From 518e0743cbcd313a2aaa1a41e4863dbc52064feb Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Wed, 27 Apr 2022 22:05:25 +1000 Subject: [PATCH 04/47] Updated config template --- InvenTree/config_template.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/config_template.yaml b/InvenTree/config_template.yaml index c60a58d37f..7ba5823386 100644 --- a/InvenTree/config_template.yaml +++ b/InvenTree/config_template.yaml @@ -194,3 +194,4 @@ static_root: '/home/inventree/data/static' # navbar_message: <h6>InvenTree demo mode <a href='https://inventree.readthedocs.io/en/latest/demo/'><span class='fas fa-info-circle'></span></a></h6> # logo: logo.png # hide_admin_link: true +# hide_password_reset: true From fc3b0621883ee67ca0a9ae4178126f9be477c097 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Wed, 27 Apr 2022 22:09:25 +1000 Subject: [PATCH 05/47] Mark injected strings as safe --- InvenTree/templates/account/login.html | 2 +- InvenTree/templates/navbar.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/templates/account/login.html b/InvenTree/templates/account/login.html index 3e0865c4b9..042c119440 100644 --- a/InvenTree/templates/account/login.html +++ b/InvenTree/templates/account/login.html @@ -36,7 +36,7 @@ for a account and sign in below:{% endblocktrans %}</p> <hr> {% if login_message %} - <div>{{ login_message }}<hr></div> + <div>{{ login_message | safe }}<hr></div> {% endif %} <div class="btn-group float-right" role="group"> <button class="btn btn-success" type="submit">{% trans "Sign In" %}</button> diff --git a/InvenTree/templates/navbar.html b/InvenTree/templates/navbar.html index 9b5ddd89a7..4660123e0d 100644 --- a/InvenTree/templates/navbar.html +++ b/InvenTree/templates/navbar.html @@ -90,7 +90,7 @@ {% if navbar_message %} {% include "spacer.html" %} <div class='flex justify-content-center'> - {{ navbar_message }} + {{ navbar_message | safe }} </div> {% include "spacer.html" %} {% include "spacer.html" %} From e809fd9a2c0a91df2ff21171635de506e15c54d8 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Wed, 27 Apr 2022 23:07:26 +1000 Subject: [PATCH 06/47] Remove database metrics from system info tab --- InvenTree/templates/stats.html | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/InvenTree/templates/stats.html b/InvenTree/templates/stats.html index 7a22f88023..e1359e5886 100644 --- a/InvenTree/templates/stats.html +++ b/InvenTree/templates/stats.html @@ -87,31 +87,4 @@ <!-- TODO - Enumerate system issues here! --> {% endfor %} {% endif %} - - <tr> - <td colspan='3'><strong>{% trans "Parts" %}</strong></td> - </tr> - <tr> - <td><span class='fas fa-sitemap'></span></td> - <td>{% trans "Part Categories" %}</td> - <td>{{ part_cat_count }}</td> - </tr> - <tr> - <td><span class='fas fa-shapes'></span></td> - <td>{% trans "Parts" %}</td> - <td>{{ part_count }}</td> - </tr> - <tr> - <td colspan="3"><strong>{% trans "Stock Items" %}</strong></td> - </tr> - <tr> - <td><span class='fas fa-map-marker-alt'></span></td> - <td>{% trans "Stock Locations" %}</td> - <td>{{ stock_loc_count }}</td> - </tr> - <tr> - <td><span class='fas fa-boxes'></span></td> - <td>{% trans "Stock Items" %}</td> - <td>{{ stock_item_count }}</td> - </tr> </table> \ No newline at end of file From 4222b614fa6a5532b1e02b2442dceca0535128ea Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Thu, 28 Apr 2022 01:20:36 +1000 Subject: [PATCH 07/47] Remove stat context variables (cherry picked from commit 0989c308d0cea9b9405a1338d257b542c6d33d73) --- InvenTree/InvenTree/views.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index feb586c844..3415a7363b 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -882,29 +882,6 @@ class DatabaseStatsView(AjaxView): ajax_template_name = "stats.html" ajax_form_title = _("System Information") - def get_context_data(self, **kwargs): - - ctx = {} - - # Part stats - ctx['part_count'] = Part.objects.count() - ctx['part_cat_count'] = PartCategory.objects.count() - - # Stock stats - ctx['stock_item_count'] = StockItem.objects.count() - ctx['stock_loc_count'] = StockLocation.objects.count() - - """ - TODO: Other ideas for database metrics - - - "Popular" parts (used to make other parts?) - - Most ordered part - - Most sold part - - etc etc etc - """ - - return ctx - class NotificationsView(TemplateView): """ View for showing notifications From 6e7b3074235bdb86938ef4f3e5ca3107d4613899 Mon Sep 17 00:00:00 2001 From: Oliver <oliver.henry.walters@gmail.com> Date: Thu, 28 Apr 2022 09:51:51 +1000 Subject: [PATCH 08/47] Prevent "null" from being displayed as part units --- InvenTree/templates/js/translated/part.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js index d552bcb9d7..57f76dcae4 100644 --- a/InvenTree/templates/js/translated/part.js +++ b/InvenTree/templates/js/translated/part.js @@ -500,6 +500,11 @@ function duplicateBom(part_id, options={}) { */ function partStockLabel(part, options={}) { + // Prevent literal string 'null' from being displayed + if (part.units == null) { + part.units = ''; + } + if (part.in_stock) { // There IS stock available for this part From 823be74f38e423c024e522c140e5a5cd6a568711 Mon Sep 17 00:00:00 2001 From: Oliver <oliver.henry.walters@gmail.com> Date: Thu, 28 Apr 2022 12:53:16 +1000 Subject: [PATCH 09/47] PEP fixes --- InvenTree/InvenTree/views.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 3415a7363b..183e491580 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -34,8 +34,7 @@ from user_sessions.views import SessionDeleteView, SessionDeleteOtherView from common.settings import currency_code_default, currency_codes -from part.models import Part, PartCategory -from stock.models import StockLocation, StockItem +from part.models import PartCategory from common.models import InvenTreeSetting, ColorTheme from users.models import check_user_role, RuleSet From aec10e0cc79e8529aa96c4a9e5bf1d25609c0393 Mon Sep 17 00:00:00 2001 From: Oliver <oliver.henry.walters@gmail.com> Date: Thu, 28 Apr 2022 14:58:05 +1000 Subject: [PATCH 10/47] Fix part allocation check --- InvenTree/part/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 1edae69351..365ed62914 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -2740,8 +2740,8 @@ class BomItem(models.Model, DataImportMixin): if not p.active: continue - # Trackable parts cannot be 'auto allocated' - if p.trackable: + # Trackable status must be the same as the sub_part + if p.trackable != self.sub_part.trackable: continue valid_parts.append(p) From ba3bcdba8916d93fac0c6e9b6d7d41caa67bf888 Mon Sep 17 00:00:00 2001 From: Oliver <oliver.henry.walters@gmail.com> Date: Thu, 28 Apr 2022 15:50:10 +1000 Subject: [PATCH 11/47] Add switchable columns to build output table --- InvenTree/templates/js/translated/build.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 65fc3a4d6c..8be3ee52ce 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -786,7 +786,7 @@ function loadBuildOutputTable(build_info, options={}) { } ); } else { - console.log(`WARNING: Could not locate sub-table for output ${pk}`); + console.warn(`Could not locate sub-table for output ${pk}`); } }); @@ -869,7 +869,7 @@ function loadBuildOutputTable(build_info, options={}) { url: '{% url "api-stock-list" %}', queryParams: filters, original: params, - showColumns: false, + showColumns: true, uniqueId: 'pk', name: 'build-outputs', sortable: true, @@ -901,6 +901,7 @@ function loadBuildOutputTable(build_info, options={}) { { field: 'part', title: '{% trans "Part" %}', + switchable: true, formatter: function(value, row) { var thumb = row.part_detail.thumbnail; @@ -909,7 +910,9 @@ function loadBuildOutputTable(build_info, options={}) { }, { field: 'quantity', - title: '{% trans "Quantity" %}', + title: '{% trans "Build Output" %}', + switchable: true, + sortable: true, formatter: function(value, row) { var url = `/stock/item/${row.pk}/`; @@ -1079,7 +1082,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { var row = $(table).bootstrapTable('getRowByUniqueId', pk); if (!row) { - console.log('WARNING: getRowByUniqueId returned null'); + console.warn('getRowByUniqueId returned null'); return; } @@ -1269,7 +1272,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { } } else { - console.log(`WARNING: Could not find progress bar for output ${outputId}`); + console.warn(`Could not find progress bar for output ${outputId}`); } } } From 63145c90b0c5fcbf0911065ea32209da980f7b09 Mon Sep 17 00:00:00 2001 From: Oliver <oliver.henry.walters@gmail.com> Date: Thu, 28 Apr 2022 16:14:23 +1000 Subject: [PATCH 12/47] Server-side sorting of "build output" column --- InvenTree/templates/js/translated/build.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 8be3ee52ce..115e55c902 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -913,6 +913,7 @@ function loadBuildOutputTable(build_info, options={}) { title: '{% trans "Build Output" %}', switchable: true, sortable: true, + sortName: 'stock', // This will sort by quantity -> serial_int -> serial formatter: function(value, row) { var url = `/stock/item/${row.pk}/`; @@ -926,7 +927,7 @@ function loadBuildOutputTable(build_info, options={}) { } return renderLink(text, url); - } + }, }, { field: 'allocated', From 2b46ed300e2e13784a6de6d96907c6ea91822517 Mon Sep 17 00:00:00 2001 From: Oliver <oliver.henry.walters@gmail.com> Date: Thu, 28 Apr 2022 16:18:40 +1000 Subject: [PATCH 13/47] Client side pagination and sorting --- InvenTree/templates/js/translated/build.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 115e55c902..5d0461861e 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -874,7 +874,7 @@ function loadBuildOutputTable(build_info, options={}) { name: 'build-outputs', sortable: true, search: false, - sidePagination: 'server', + sidePagination: 'client', detailView: has_tracked_items, detailFilter: function(index, row) { return true; @@ -913,7 +913,6 @@ function loadBuildOutputTable(build_info, options={}) { title: '{% trans "Build Output" %}', switchable: true, sortable: true, - sortName: 'stock', // This will sort by quantity -> serial_int -> serial formatter: function(value, row) { var url = `/stock/item/${row.pk}/`; @@ -928,6 +927,21 @@ function loadBuildOutputTable(build_info, options={}) { return renderLink(text, url); }, + sorter: function(a, b, row_a, row_b) { + // Sort first by quantity, and then by serial number + if ((row_a.quantity > 1) || (row_b.quantity > 1)) { + return row_a.quantity > row_b.quantity ? 1 : -1; + } + + if ((row_a.serial != null) && (row_b.serial != null)) { + var sn_a = Number.parseInt(row_a.serial) || 0; + var sn_b = Number.parseInt(row_b.serial) || 0; + + return sn_a > sn_b ? 1 : -1; + } + + return 0; + } }, { field: 'allocated', From 6538ab86cb11c0e249b8861861bf4cb042ab3bed Mon Sep 17 00:00:00 2001 From: Oliver <oliver.henry.walters@gmail.com> Date: Thu, 28 Apr 2022 16:25:04 +1000 Subject: [PATCH 14/47] Bug fix for 'required' filter in PartTestTemplate API --- InvenTree/part/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 1a80c87322..3752f7daf4 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -383,7 +383,7 @@ class PartTestTemplateList(generics.ListCreateAPIView): required = params.get('required', None) if required is not None: - queryset = queryset.filter(required=required) + queryset = queryset.filter(required=str2bool(required)) return queryset From d894302e6258779c378d12d0cd24363ac2361a8b Mon Sep 17 00:00:00 2001 From: Oliver <oliver.henry.walters@gmail.com> Date: Thu, 28 Apr 2022 16:54:02 +1000 Subject: [PATCH 15/47] Request build output test result information --- InvenTree/templates/js/translated/build.js | 131 ++++++++++++++++++++- 1 file changed, 128 insertions(+), 3 deletions(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 5d0461861e..6a4e7ac429 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -865,6 +865,87 @@ function loadBuildOutputTable(build_info, options={}) { ); } + var part_tests = null; + + function updateTestResultData(rows) { + + console.log("updateTestResultData"); + + // Request test template data if it has not already been retrieved + if (part_tests == null) { + inventreeGet( + '{% url "api-part-test-template-list" %}', + { + part: build_info.part, + required: true, + }, + { + async: false, + success: function(response) { + // Save the list of part tests + part_tests = response; + + updateTestResultData(rows); + } + } + );; + } + + rows.forEach(function(row) { + + // Ignore if this row has already been updated (else, infinite loop!) + if (row.passed_tests) { + return; + } + + // Request test result information for the particular build output + inventreeGet( + '{% url "api-stock-test-result-list" %}', + { + stock_item: row.pk, + }, + { + success: function(results) { + + // A list of tests that this stock item has passed + var passed_tests = {}; + + // Keep a list of tests that this stock item has passed + results.forEach(function(result) { + if (result.result) { + passed_tests[result.key] = true; + } + }); + + row.passed_tests = passed_tests; + + $(table).bootstrapTable('updateByUniqueId', row.pk, row, true); + } + } + ) + }); + } + + // Return the number of 'passed' tests in a given row + function countPassedTests(row) { + if (part_tests == null) { + return 0; + } + + var results = row.passed_tests || {}; + var n = 0; + + part_tests.forEach(function(test) { + if (results[test.key] || false) { + n += 1; + } + }); + + return n; + } + + var table_loaded = false; + $(table).inventreeTable({ url: '{% url "api-stock-list" %}', queryParams: filters, @@ -885,11 +966,21 @@ function loadBuildOutputTable(build_info, options={}) { formatNoMatches: function() { return '{% trans "No active build outputs found" %}'; }, - onPostBody: function() { + onPostBody: function(rows) { + console.log("onPostBody"); // Add callbacks for the buttons setupBuildOutputButtonCallbacks(); + }, + onLoadSuccess: function(rows) { - $(table).bootstrapTable('expandAllRows'); + console.log("onLoadSuccess"); + + updateTestResultData(rows); + }, + onRefresh: function() { + console.log("onRefresh"); + // var rows = $(table).bootstrapTable('getData'); + // updateTestResultData(rows); }, columns: [ { @@ -945,10 +1036,44 @@ function loadBuildOutputTable(build_info, options={}) { }, { field: 'allocated', - title: '{% trans "Allocated Parts" %}', + title: '{% trans "Allocated Stock" %}', visible: has_tracked_items, + switchable: false, formatter: function(value, row) { return `<div id='output-progress-${row.pk}'><span class='fas fa-spin fa-spinner'></span></div>`; + }, + sorter: function(value_a, value_b, row_a, row_b) { + // TODO: Custom sorter for "allocated stock" column + return 0; + }, + }, + { + field: 'tests', + title: '{% trans "Tests" %}', + sortable: false, + switchable: true, + formatter: function(value, row) { + if (part_tests == null || part_tests.length == 0) { + return `<em>{% trans "No tests found " %}</em>`; + } + + var n_passed = countPassedTests(row); + + var progress = makeProgressBar( + n_passed, + part_tests.length, + { + max_width: '150px', + } + ); + + return progress; + }, + sorter: function(a, b, row_a, row_b) { + var n_a = countPassedTests(row_a); + var n_b = countPassedTests(row_b); + + return n_a > n_b ? 1 : -1; } }, { From a48c8025760cb38c2a444ebeea1cf398adcd38b1 Mon Sep 17 00:00:00 2001 From: Oliver <oliver.henry.walters@gmail.com> Date: Thu, 28 Apr 2022 16:54:29 +1000 Subject: [PATCH 16/47] Sort by test status --- InvenTree/templates/js/translated/build.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 6a4e7ac429..111cb51b15 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -1050,7 +1050,7 @@ function loadBuildOutputTable(build_info, options={}) { { field: 'tests', title: '{% trans "Tests" %}', - sortable: false, + sortable: true, switchable: true, formatter: function(value, row) { if (part_tests == null || part_tests.length == 0) { From 74a08be5be0759e28629e478e0cf5401a358be7f Mon Sep 17 00:00:00 2001 From: Oliver <oliver.henry.walters@gmail.com> Date: Thu, 28 Apr 2022 17:00:23 +1000 Subject: [PATCH 17/47] Load (and cache) tracked BOM items for this build output --- InvenTree/templates/js/translated/build.js | 46 +++++++++++++++++++--- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 111cb51b15..f7399a50df 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -865,9 +865,44 @@ function loadBuildOutputTable(build_info, options={}) { ); } + // List of "tracked bom items" required for this build order + var bom_items = null; + + function updateAllocationData(rows) { + // Update stock allocation information for the build outputs + + console.log("updateAllocationData"); + + // Request list of BOM data for this build order + if (bom_items == null) { + inventreeGet( + '{% url "api-bom-list" %}', + { + part: build_info.part, + sub_part_detail: true, + sub_part_trackable: true, + }, + { + success: function(response) { + // Save the BOM items + bom_items = response; + + // Callback to this function again + updateAllocationData(rows); + } + } + ); + + return; + } + + console.log("BOM ITEMS:", bom_items); + } + var part_tests = null; function updateTestResultData(rows) { + // Update test result information for the build outputs console.log("updateTestResultData"); @@ -880,15 +915,17 @@ function loadBuildOutputTable(build_info, options={}) { required: true, }, { - async: false, success: function(response) { // Save the list of part tests part_tests = response; + // Callback to this function again updateTestResultData(rows); } } - );; + ); + + return; } rows.forEach(function(row) { @@ -944,8 +981,6 @@ function loadBuildOutputTable(build_info, options={}) { return n; } - var table_loaded = false; - $(table).inventreeTable({ url: '{% url "api-stock-list" %}', queryParams: filters, @@ -975,12 +1010,11 @@ function loadBuildOutputTable(build_info, options={}) { console.log("onLoadSuccess"); + updateAllocationData(rows); updateTestResultData(rows); }, onRefresh: function() { console.log("onRefresh"); - // var rows = $(table).bootstrapTable('getData'); - // updateTestResultData(rows); }, columns: [ { From e6c95bf6b21f219a5e6b7d96ba493fa95a385570 Mon Sep 17 00:00:00 2001 From: Oliver <oliver.henry.walters@gmail.com> Date: Thu, 28 Apr 2022 17:08:43 +1000 Subject: [PATCH 18/47] Cache tracked BOM items for the build order --- InvenTree/templates/js/translated/build.js | 50 +++++++++++++--------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index f7399a50df..f8872f12a5 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -841,6 +841,9 @@ function loadBuildOutputTable(build_info, options={}) { }); } + // List of "tracked bom items" required for this build order + var bom_items = null; + /* * Construct a "sub table" showing the required BOM items */ @@ -855,6 +858,9 @@ function loadBuildOutputTable(build_info, options={}) { element.html(html); + // Pass through the cached BOM items + build_info.bom_items = bom_items; + loadBuildOutputAllocationTable( build_info, row, @@ -865,14 +871,9 @@ function loadBuildOutputTable(build_info, options={}) { ); } - // List of "tracked bom items" required for this build order - var bom_items = null; - function updateAllocationData(rows) { // Update stock allocation information for the build outputs - console.log("updateAllocationData"); - // Request list of BOM data for this build order if (bom_items == null) { inventreeGet( @@ -896,7 +897,9 @@ function loadBuildOutputTable(build_info, options={}) { return; } - console.log("BOM ITEMS:", bom_items); + rows.forEach(function(row) { + + }) } var part_tests = null; @@ -904,8 +907,6 @@ function loadBuildOutputTable(build_info, options={}) { function updateTestResultData(rows) { // Update test result information for the build outputs - console.log("updateTestResultData"); - // Request test template data if it has not already been retrieved if (part_tests == null) { inventreeGet( @@ -1002,20 +1003,14 @@ function loadBuildOutputTable(build_info, options={}) { return '{% trans "No active build outputs found" %}'; }, onPostBody: function(rows) { - console.log("onPostBody"); // Add callbacks for the buttons setupBuildOutputButtonCallbacks(); }, onLoadSuccess: function(rows) { - console.log("onLoadSuccess"); - updateAllocationData(rows); updateTestResultData(rows); }, - onRefresh: function() { - console.log("onRefresh"); - }, columns: [ { title: '', @@ -1320,14 +1315,29 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { }); } + var bom_items = buildInfo.bom_items || null; + + // If BOM items have not been provided, load via the API + if (bom_items == null) { + inventreeGet( + '{% url "api-bom-list" %}', + { + part: partId, + sub_part_detail: true, + sub_part_trackable: trackable, + }, + { + async: false, + success: function(results) { + bom_items = results; + } + } + ); + } + // Load table of BOM items $(table).inventreeTable({ - url: '{% url "api-bom-list" %}', - queryParams: { - part: partId, - sub_part_detail: true, - sub_part_trackable: trackable, - }, + data: bom_items, disablePagination: true, formatNoMatches: function() { return '{% trans "No BOM items found" %}'; From 0841c628e0ba8626aad14882e76753ede63b0c8c Mon Sep 17 00:00:00 2001 From: Oliver <oliver.henry.walters@gmail.com> Date: Thu, 28 Apr 2022 17:27:09 +1000 Subject: [PATCH 19/47] Adds ability to filter build items by "tracked" flag --- InvenTree/build/api.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index 33f3f4ab36..2e2fb53510 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -442,6 +442,18 @@ class BuildItemList(generics.ListCreateAPIView): if part_pk: queryset = queryset.filter(stock_item__part=part_pk) + # Filter by "tracked" status + # Tracked means that the item is "installed" into a build output (stock item) + tracked = params.get('tracked', None) + + if tracked is not None: + tracked = str2bool(tracked) + + if tracked: + queryset = queryset.exclude(install_into=None) + else: + queryset = queryest.filter(install_into=None) + # Filter by output target output = params.get('output', None) From b8f274c680131247cf4a3ff9e59f2421f1c50bec Mon Sep 17 00:00:00 2001 From: Oliver <oliver.henry.walters@gmail.com> Date: Thu, 28 Apr 2022 17:38:08 +1000 Subject: [PATCH 20/47] Request allocations for entire build, and cache --- InvenTree/templates/js/translated/build.js | 73 ++++++++++++++++++++-- 1 file changed, 68 insertions(+), 5 deletions(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index f8872f12a5..aa35d4c011 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -897,9 +897,46 @@ function loadBuildOutputTable(build_info, options={}) { return; } - rows.forEach(function(row) { + // Request updated stock allocation data for this build order + inventreeGet( + '{% url "api-build-item-list" %}', + { + build: build_info.pk, + part_detail: true, + location_detail: true, + sub_part_trackable: true, + tracked: true, + }, + { + success: function(response) { - }) + // Group allocation information by the "install_into" field + var allocations = {}; + + response.forEach(function(allocation) { + var target = allocation.install_into; + + if (target != null) { + if (!(target in allocations)) { + allocations[target] = []; + } + + allocations[target].push(allocation); + } + }); + + // Now that the allocations have been grouped by stock item, + // we can update each row in the table, + // using the pk value of each row (stock item) + rows.forEach(function(row) { + row.allocations = allocations[row.pk] || []; + $(table).bootstrapTable('updateByUniqueId', row.pk, row, true); + + console.log("Updating row for stock item", row.pk); + }); + } + } + ) } var part_tests = null; @@ -1145,7 +1182,6 @@ function loadBuildOutputTable(build_info, options={}) { */ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { - var buildId = buildInfo.pk; var partId = buildInfo.part; @@ -1178,6 +1214,30 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { // Otherwise, only "untrackable" parts are allowed var trackable = ! !output; + var allocated_items = output == null ? null : output.allocations; + + if (allocated_items == null) { + + inventreeGet( + '{% url "api-build-item-list" %}', + { + build: buildId, + part_detail: true, + location_detail: true, + output: output == null ? null : output.pk, + }, + { + async: false, + success: function(response) { + allocated_items = response; + } + } + ); + } + + console.log("rendering table for output:", outputId); + console.log("allocations:", allocated_items); + function reloadTable() { // Reload the entire build allocation table $(table).bootstrapTable('refresh'); @@ -1348,10 +1408,13 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { onPostBody: function(data) { // Setup button callbacks setupCallbacks(); - }, - onLoadSuccess: function(tableData) { + // }, + // onLoadSuccess: function(tableData) { // Once the BOM data are loaded, request allocation data for this build output + console.log("old onLoadSuccess"); + return; + var params = { build: buildId, part_detail: true, From 72bcea2f6dac73851a5c4da95c2d6aab01b60e38 Mon Sep 17 00:00:00 2001 From: Oliver <oliver.henry.walters@gmail.com> Date: Thu, 28 Apr 2022 17:53:27 +1000 Subject: [PATCH 21/47] Better caching and rendering of sub tables for particular build outputs --- InvenTree/templates/js/translated/build.js | 172 ++++----------------- 1 file changed, 28 insertions(+), 144 deletions(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index aa35d4c011..820f78f47b 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -931,8 +931,6 @@ function loadBuildOutputTable(build_info, options={}) { rows.forEach(function(row) { row.allocations = allocations[row.pk] || []; $(table).bootstrapTable('updateByUniqueId', row.pk, row, true); - - console.log("Updating row for stock item", row.pk); }); } } @@ -1280,24 +1278,37 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { } - function sumAllocations(row) { - // Calculat total allocations for a given row - if (!row.allocations) { - row.allocated = 0; - return 0; - } + function getAllocationsForRow(row) { + var part_id = row.sub_part; - var quantity = 0; + var allocations = []; - row.allocations.forEach(function(item) { - quantity += item.quantity; + allocated_items.forEach(function(allocation) { + if (allocation.bom_part == part_id) { + allocations.push(allocation); + } }); - row.allocated = parseFloat(quantity.toFixed(15)); + return allocations; + } + + function sumAllocations(row) { + + var allocated_quantity = 0; + + getAllocationsForRow(row).forEach(function(allocation) { + allocated_quantity += allocation.quantity; + }); + + row.allocated = parseFloat(allocated_quantity.toFixed(15)); return row.allocated; } + function isRowFullyAllocated(row) { + return sumAllocations(row) >= requiredQuantity(row); + } + function setupCallbacks() { // Register button callbacks once table data are loaded @@ -1408,128 +1419,12 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { onPostBody: function(data) { // Setup button callbacks setupCallbacks(); - // }, - // onLoadSuccess: function(tableData) { - // Once the BOM data are loaded, request allocation data for this build output - - console.log("old onLoadSuccess"); - return; - - var params = { - build: buildId, - part_detail: true, - location_detail: true, - }; - - if (output) { - params.sub_part_trackable = true; - params.output = outputId; - } else { - params.sub_part_trackable = false; - } - - inventreeGet('/api/build/item/', - params, - { - success: function(data) { - // Iterate through the returned data, and group by the part they point to - var allocations = {}; - - // Total number of line items - var totalLines = tableData.length; - - // Total number of "completely allocated" lines - var allocatedLines = 0; - - data.forEach(function(item) { - - // Group BuildItem objects by part - var part = item.bom_part || item.part; - var key = parseInt(part); - - if (!(key in allocations)) { - allocations[key] = []; - } - - allocations[key].push(item); - }); - - // Now update the allocations for each row in the table - for (var key in allocations) { - - // Select the associated row in the table - var tableRow = $(table).bootstrapTable('getRowByUniqueId', key); - - if (!tableRow) { - continue; - } - - // Set the allocation list for that row - tableRow.allocations = allocations[key]; - - // Calculate the total allocated quantity - var allocatedQuantity = sumAllocations(tableRow); - - var requiredQuantity = 0; - - if (output) { - requiredQuantity = tableRow.quantity * output.quantity; - } else { - requiredQuantity = tableRow.quantity * buildInfo.quantity; - } - - // Is this line item fully allocated? - if (allocatedQuantity >= requiredQuantity) { - allocatedLines += 1; - } - - // Push the updated row back into the main table - $(table).bootstrapTable('updateByUniqueId', key, tableRow, true); - } - - // Update any rows which we did not receive allocation information for - var td = $(table).bootstrapTable('getData'); - - td.forEach(function(tableRow) { - if (tableRow.allocations == null) { - - tableRow.allocations = []; - - $(table).bootstrapTable('updateByUniqueId', tableRow.pk, tableRow, true); - } - }); - - // Update the progress bar for this build output - var build_progress = $(`#output-progress-${outputId}`); - - if (build_progress.exists()) { - if (totalLines > 0) { - - var progress = makeProgressBar( - allocatedLines, - totalLines, - { - max_width: '150px', - } - ); - - build_progress.html(progress); - } else { - build_progress.html(''); - } - - } else { - console.warn(`Could not find progress bar for output ${outputId}`); - } - } - } - ); }, sortable: true, showColumns: false, detailView: true, detailFilter: function(index, row) { - return row.allocations != null; + return sumAllocations(row) > 0; }, detailFormatter: function(index, row, element) { // Contruct an 'inner table' which shows which stock items have been allocated @@ -1543,7 +1438,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { var subTable = $(`#${subTableId}`); subTable.bootstrapTable({ - data: row.allocations, + data: getAllocationsForRow(row), showHeader: true, columns: [ { @@ -1565,7 +1460,6 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { var url = ''; - var serial = row.serial; if (row.stock_item_detail) { @@ -1744,19 +1638,9 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { title: '{% trans "Allocated" %}', sortable: true, formatter: function(value, row) { - var allocated = 0; - - if (row.allocations != null) { - row.allocations.forEach(function(item) { - allocated += item.quantity; - }); - - var required = requiredQuantity(row); - - return makeProgressBar(allocated, required); - } else { - return `<em>{% trans "loading" %}...</em><span class='fas fa-spinner fa-spin float-right'></span>`; - } + var allocated = sumAllocations(row); + var required = requiredQuantity(row) + return makeProgressBar(allocated, required); }, sorter: function(valA, valB, rowA, rowB) { // Custom sorting function for progress bars From 8077469a3f5aa9f0ecb95d1994720328ae868bed Mon Sep 17 00:00:00 2001 From: Paul R <exp@users.noreply.github.com> Date: Thu, 28 Apr 2022 10:32:13 +0100 Subject: [PATCH 22/47] [#2885] Support multiple '~' placeholders in serial numbers --- InvenTree/InvenTree/helpers.py | 5 +++-- InvenTree/InvenTree/tests.py | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index c5a8ad4b67..b94383c874 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -427,8 +427,9 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int): serials = serials.strip() # fill in the next serial number into the serial - if '~' in serials: - serials = serials.replace('~', str(next_number)) + while '~' in serials: + serials = serials.replace('~', str(next_number), 1) + next_number += 1 # Split input string by whitespace or comma (,) characters groups = re.split("[\s,]+", serials) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index 669628bdea..11ca990b8e 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -252,6 +252,11 @@ class TestSerialNumberExtraction(TestCase): sn = e("1, 2, 3, 4, 5", 5, 1) self.assertEqual(len(sn), 5) + # Test multiple placeholders + sn = e("1 2 ~ ~ ~", 5, 3) + self.assertEqual(len(sn), 5) + self.assertEqual(sn, [1, 2, 3, 4, 5]) + sn = e("1-5, 10-15", 11, 1) self.assertIn(3, sn) self.assertIn(13, sn) From 1dba9f66fb4fd4787c1619f3c7431e087e8c3cfc Mon Sep 17 00:00:00 2001 From: Paul R <exp@users.noreply.github.com> Date: Thu, 28 Apr 2022 10:34:01 +0100 Subject: [PATCH 23/47] [#2885] Support partial sequences in serial nos ('1, 2, 3+') --- InvenTree/InvenTree/helpers.py | 16 ++++++++-------- InvenTree/InvenTree/tests.py | 5 +++++ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index b94383c874..45030ec0d7 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -439,6 +439,12 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int): # Helper function to check for duplicated numbers def add_sn(sn): + # Attempt integer conversion first, so numerical strings are never stored + try: + sn = int(sn) + except ValueError: + pass + if sn in numbers: errors.append(_('Duplicate serial: {sn}').format(sn=sn)) else: @@ -496,7 +502,7 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int): # case 1 else: - end = start + expected_quantity + end = start + (expected_quantity - len(numbers)) for n in range(start, end): add_sn(n) @@ -506,13 +512,7 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int): # At this point, we assume that the "group" is just a single serial value elif group: - - try: - # First attempt to add as an integer value - add_sn(int(group)) - except (ValueError): - # As a backup, add as a string value - add_sn(group) + add_sn(group) # No valid input group detected else: diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index 11ca990b8e..b9e9dd5228 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -252,6 +252,11 @@ class TestSerialNumberExtraction(TestCase): sn = e("1, 2, 3, 4, 5", 5, 1) self.assertEqual(len(sn), 5) + # Test partially specifying serials + sn = e("1, 2, 4+", 5, 1) + self.assertEqual(len(sn), 5) + self.assertEqual(sn, [1, 2, 4, 5, 6]) + # Test multiple placeholders sn = e("1 2 ~ ~ ~", 5, 3) self.assertEqual(len(sn), 5) From b08efa4de7217dc9a2fcd81cc4879178cfaf872e Mon Sep 17 00:00:00 2001 From: Paul R <exp@users.noreply.github.com> Date: Thu, 28 Apr 2022 10:38:04 +0100 Subject: [PATCH 24/47] [#2885] Don't interpolate serial groups if they are not numeric --- InvenTree/InvenTree/helpers.py | 5 +++-- InvenTree/InvenTree/tests.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index 45030ec0d7..68f42bab87 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -466,7 +466,7 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int): if '-' in group: items = group.split('-') - if len(items) == 2: + if len(items) == 2 and all([i.isnumeric() for i in items]): a = items[0].strip() b = items[1].strip() @@ -484,7 +484,8 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int): errors.append(_("Invalid group: {g}").format(g=group)) continue else: - errors.append(_("Invalid group: {g}").format(g=group)) + # More than 2 hyphens or non-numeric group so add without interpolating + add_sn(group) # plus signals either # 1: 'start+': expected number of serials, starting at start diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index b9e9dd5228..51e5527535 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -257,6 +257,16 @@ class TestSerialNumberExtraction(TestCase): self.assertEqual(len(sn), 5) self.assertEqual(sn, [1, 2, 4, 5, 6]) + # Test groups are not interpolated with more than one hyphen in a word + sn = e("1, 2, TG-4SR-92, 4+", 5, 1) + self.assertEqual(len(sn), 5) + self.assertEqual(sn, [1, 2, "TG-4SR-92", 4, 5]) + + # Test groups are not interpolated with alpha characters + sn = e("1, A-2, 3+", 5, 1) + self.assertEqual(len(sn), 5) + self.assertEqual(sn, [1, "A-2", 3, 4, 5]) + # Test multiple placeholders sn = e("1 2 ~ ~ ~", 5, 3) self.assertEqual(len(sn), 5) @@ -317,6 +327,10 @@ class TestSerialNumberExtraction(TestCase): with self.assertRaises(ValidationError): e("10, a, 7-70j", 4, 1) + # Test groups are not interpolated with word characters + with self.assertRaises(ValidationError): + e("1, 2, 3, E-5", 5, 1) + def test_combinations(self): e = helpers.extract_serial_numbers From 82b32fe30e61bc9613d7a4e047864ad0ef491437 Mon Sep 17 00:00:00 2001 From: Paul R <exp@users.noreply.github.com> Date: Thu, 28 Apr 2022 10:38:34 +0100 Subject: [PATCH 25/47] [#2885] Support hyphens in serials when correct no of serials supplied --- InvenTree/InvenTree/helpers.py | 12 +++++++++++- InvenTree/InvenTree/tests.py | 5 +++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index 68f42bab87..675898e3d0 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -458,8 +458,18 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int): if len(serials) == 0: raise ValidationError([_("Empty serial number string")]) - for group in groups: + # If the user has supplied the correct number of serials, don't process them for groups + # just add them so any duplicates (or future validations) are checked + if len(groups) == expected_quantity: + for group in groups: + add_sn(group) + if len(errors) > 0: + raise ValidationError(errors) + + return numbers + + for group in groups: group = group.strip() # Hyphen indicates a range of numbers diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index 51e5527535..13f9198d92 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -257,6 +257,11 @@ class TestSerialNumberExtraction(TestCase): self.assertEqual(len(sn), 5) self.assertEqual(sn, [1, 2, 4, 5, 6]) + # Test groups are not interpolated if enough serials are supplied + sn = e("1, 2, 3, AF5-69H, 5", 5, 1) + self.assertEqual(len(sn), 5) + self.assertEqual(sn, [1, 2, 3, "AF5-69H", 5]) + # Test groups are not interpolated with more than one hyphen in a word sn = e("1, 2, TG-4SR-92, 4+", 5, 1) self.assertEqual(len(sn), 5) From 9ce2eb988f5c9d82e30a4244c07165fb8bbd63dc Mon Sep 17 00:00:00 2001 From: Paul R <exp@users.noreply.github.com> Date: Thu, 28 Apr 2022 11:32:20 +0100 Subject: [PATCH 26/47] [#2885] Modify group error messages to be unique --- InvenTree/InvenTree/helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index 675898e3d0..7bd4fd819d 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -488,7 +488,7 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int): for n in range(a, b + 1): add_sn(n) else: - errors.append(_("Invalid group: {g}").format(g=group)) + errors.append(_("Invalid group range: {g}").format(g=group)) except ValueError: errors.append(_("Invalid group: {g}").format(g=group)) @@ -519,7 +519,7 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int): add_sn(n) # no case else: - errors.append(_("Invalid group: {g}").format(g=group)) + errors.append(_("Invalid group sequence: {g}").format(g=group)) # At this point, we assume that the "group" is just a single serial value elif group: From f531e354b95ace9e0673a9b3c27756497cdf7c80 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Thu, 28 Apr 2022 21:54:38 +1000 Subject: [PATCH 27/47] Allow completion of partially allocated build outputs - Requires manual acceptance from user --- InvenTree/build/api.py | 2 +- InvenTree/build/serializers.py | 15 ++++++++++++++- InvenTree/templates/js/translated/build.js | 6 ++++-- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index 2e2fb53510..22dd6473ab 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -452,7 +452,7 @@ class BuildItemList(generics.ListCreateAPIView): if tracked: queryset = queryset.exclude(install_into=None) else: - queryset = queryest.filter(install_into=None) + queryset = queryset.filter(install_into=None) # Filter by output target output = params.get('output', None) diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 4b55182563..7df2c474a9 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -161,7 +161,12 @@ class BuildOutputSerializer(serializers.Serializer): # The build output must have all tracked parts allocated if not build.is_fully_allocated(output): - raise ValidationError(_("This build output is not fully allocated")) + + # Check if the user has specified that incomplete allocations are ok + accept_incomplete = InvenTree.helpers.str2bool(self.context['request'].data.get('accept_incomplete_allocation', False)) + + if not accept_incomplete: + raise ValidationError(_("This build output is not fully allocated")) return output @@ -355,6 +360,7 @@ class BuildOutputCompleteSerializer(serializers.Serializer): 'outputs', 'location', 'status', + 'accept_incomplete_allocation', 'notes', ] @@ -377,6 +383,13 @@ class BuildOutputCompleteSerializer(serializers.Serializer): label=_("Status"), ) + accept_incomplete_allocation = serializers.BooleanField( + default=False, + required=False, + label=_('Accept Incomplete Allocation'), + help_text=_('Complete ouputs if stock has not been fully allocated'), + ) + notes = serializers.CharField( label=_("Notes"), required=False, diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 820f78f47b..29683154fe 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -427,6 +427,8 @@ function completeBuildOutputs(build_id, outputs, options={}) { fields: { status: {}, location: {}, + notes: {}, + accept_incomplete_allocation: {}, }, confirm: true, title: '{% trans "Complete Build Outputs" %}', @@ -445,6 +447,8 @@ function completeBuildOutputs(build_id, outputs, options={}) { outputs: [], status: getFormFieldValue('status', {}, opts), location: getFormFieldValue('location', {}, opts), + notes: getFormFieldValue('notes', {}, opts), + accept_incomplete_allocation: getFormFieldValue('accept_incomplete_allocation', {type: 'boolean'}, opts), }; var output_pk_values = []; @@ -1896,8 +1900,6 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) { method: 'POST', fields: {}, preFormContent: html, - confirm: true, - confirmMessage: '{% trans "Confirm stock allocation" %}', title: '{% trans "Allocate Stock Items to Build Order" %}', afterRender: function(fields, options) { From b63352ce200ce60f15a773bbd73644f296c7c16c Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Thu, 28 Apr 2022 22:58:58 +1000 Subject: [PATCH 28/47] Add a stock item transaction when installing items into a build output --- InvenTree/build/models.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index e5189e6073..61783fc8fb 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -1260,7 +1260,7 @@ class BuildItem(models.Model): }) @transaction.atomic - def complete_allocation(self, user): + def complete_allocation(self, user, notes=''): """ Complete the allocation of this BuildItem into the output stock item. @@ -1286,8 +1286,13 @@ class BuildItem(models.Model): self.save() # Install the stock item into the output - item.belongs_to = self.install_into - item.save() + self.install_into.installStockItem( + item, + self.quantity, + user, + notes + ) + else: # Simply remove the items from stock item.take_stock( From cb7c4396fb160043a0946ee1bea15721f875363b Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Thu, 28 Apr 2022 22:59:24 +1000 Subject: [PATCH 29/47] Refactor build page template - Only load build outputs table as required --- InvenTree/build/templates/build/detail.html | 177 ++++++++++---------- InvenTree/templates/js/translated/build.js | 16 +- 2 files changed, 89 insertions(+), 104 deletions(-) diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index 92e1177e0f..c01c9055f1 100644 --- a/InvenTree/build/templates/build/detail.html +++ b/InvenTree/build/templates/build/detail.html @@ -401,110 +401,107 @@ function reloadTable() { $('#allocation-table-untracked').bootstrapTable('refresh'); } -// Get the list of BOM items required for this build -inventreeGet( - '{% url "api-bom-list" %}', - { +onPanelLoad('outputs', function() { + {% if build.active %} + + var build_info = { + pk: {{ build.pk }}, part: {{ build.part.pk }}, - sub_part_detail: true, - }, - { - success: function(response) { + quantity: {{ build.quantity }}, + {% if build.take_from %} + source_location: {{ build.take_from.pk }}, + {% endif %} + tracked_parts: true, + }; - var build_info = { - pk: {{ build.pk }}, - part: {{ build.part.pk }}, - quantity: {{ build.quantity }}, - bom_items: response, - {% if build.take_from %} - source_location: {{ build.take_from.pk }}, - {% endif %} - {% if build.has_tracked_bom_items %} - tracked_parts: true, - {% else %} - tracked_parts: false, - {% endif %} - }; + loadBuildOutputTable(build_info); - {% if build.active %} - loadBuildOutputTable(build_info); - linkButtonsToSelection( - '#build-output-table', - [ - '#output-options', - '#multi-output-complete', - '#multi-output-delete', - ] - ); + linkButtonsToSelection( + '#build-output-table', + [ + '#output-options', + '#multi-output-complete', + '#multi-output-delete', + ] + ); - $('#multi-output-complete').click(function() { - var outputs = $('#build-output-table').bootstrapTable('getSelections'); + $('#multi-output-complete').click(function() { + var outputs = $('#build-output-table').bootstrapTable('getSelections'); - completeBuildOutputs( - build_info.pk, - outputs, - { - success: function() { - // Reload the "in progress" table - $('#build-output-table').bootstrapTable('refresh'); + completeBuildOutputs( + build_info.pk, + outputs, + { + success: function() { + // Reload the "in progress" table + $('#build-output-table').bootstrapTable('refresh'); - // Reload the "completed" table - $('#build-stock-table').bootstrapTable('refresh'); - } - } - ); - }); - - $('#multi-output-delete').click(function() { - var outputs = $('#build-output-table').bootstrapTable('getSelections'); - - deleteBuildOutputs( - build_info.pk, - outputs, - { - success: function() { - // Reload the "in progress" table - $('#build-output-table').bootstrapTable('refresh'); - - // Reload the "completed" table - $('#build-stock-table').bootstrapTable('refresh'); - } - } - ) - }); - - $('#incomplete-output-print-label').click(function() { - var outputs = $('#build-output-table').bootstrapTable('getSelections'); - - if (outputs.length == 0) { - outputs = $('#build-output-table').bootstrapTable('getData'); + // Reload the "completed" table + $('#build-stock-table').bootstrapTable('refresh'); } + } + ); + }); - var stock_id_values = []; + $('#multi-output-delete').click(function() { + var outputs = $('#build-output-table').bootstrapTable('getSelections'); - outputs.forEach(function(output) { - stock_id_values.push(output.pk); - }); + deleteBuildOutputs( + build_info.pk, + outputs, + { + success: function() { + // Reload the "in progress" table + $('#build-output-table').bootstrapTable('refresh'); - printStockItemLabels(stock_id_values); - - }); - - {% endif %} - - {% if build.active and build.has_untracked_bom_items %} - // Load allocation table for un-tracked parts - loadBuildOutputAllocationTable( - build_info, - null, - { - search: true, + // Reload the "completed" table + $('#build-stock-table').bootstrapTable('refresh'); } - ); - {% endif %} + } + ) + }); + + $('#incomplete-output-print-label').click(function() { + var outputs = $('#build-output-table').bootstrapTable('getSelections'); + + if (outputs.length == 0) { + outputs = $('#build-output-table').bootstrapTable('getData'); } + + var stock_id_values = []; + + outputs.forEach(function(output) { + stock_id_values.push(output.pk); + }); + + printStockItemLabels(stock_id_values); + + }); + + {% endif %} +}); + +{% if build.active and build.has_untracked_bom_items %} + +var build_info = { + pk: {{ build.pk }}, + part: {{ build.part.pk }}, + quantity: {{ build.quantity }}, + {% if build.take_from %} + source_location: {{ build.take_from.pk }}, + {% endif %} + tracked_parts: false, +}; + +// Load allocation table for un-tracked parts +loadBuildOutputAllocationTable( + build_info, + null, + { + search: true, } ); +{% endif %} $('#btn-create-output').click(function() { diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 29683154fe..b90837a231 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -741,18 +741,6 @@ function loadBuildOutputTable(build_info, options={}) { params.is_building = true; params.build = build_info.pk; - // Construct a list of "tracked" BOM items - var tracked_bom_items = []; - - var has_tracked_items = false; - - build_info.bom_items.forEach(function(bom_item) { - if (bom_item.sub_part_detail.trackable) { - tracked_bom_items.push(bom_item); - has_tracked_items = true; - }; - }); - var filters = {}; for (var key in params) { @@ -1031,7 +1019,7 @@ function loadBuildOutputTable(build_info, options={}) { sortable: true, search: false, sidePagination: 'client', - detailView: has_tracked_items, + detailView: true, detailFilter: function(index, row) { return true; }, @@ -1105,7 +1093,7 @@ function loadBuildOutputTable(build_info, options={}) { { field: 'allocated', title: '{% trans "Allocated Stock" %}', - visible: has_tracked_items, + visible: true, switchable: false, formatter: function(value, row) { return `<div id='output-progress-${row.pk}'><span class='fas fa-spin fa-spinner'></span></div>`; From 4a81c058d60c46978ace380ae90d16fd3fc6a0df Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Thu, 28 Apr 2022 23:14:37 +1000 Subject: [PATCH 30/47] Function to reload allocation table data --- InvenTree/templates/js/translated/build.js | 41 ++++++++++++++++++---- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index b90837a231..7705bfaa16 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -342,7 +342,9 @@ function unallocateStock(build_id, options={}) { }, title: '{% trans "Unallocate Stock Items" %}', onSuccess: function(response, opts) { - if (options.table) { + if (options.onSuccess) { + options.onSuccess(response, opts); + } else if (options.table) { // Reload the parent table $(options.table).bootstrapTable('refresh'); } @@ -1206,7 +1208,8 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { var allocated_items = output == null ? null : output.allocations; - if (allocated_items == null) { + function reloadAllocationData(async=true) { + // Reload stock allocation data for this particular build output inventreeGet( '{% url "api-build-item-list" %}', @@ -1217,16 +1220,37 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { output: output == null ? null : output.pk, }, { - async: false, + async: async, success: function(response) { allocated_items = response; + + if (async) { + + // Force a refresh of each row in the table + // Note we cannot call 'refresh' because we are passing data from memory + var rows = $(table).bootstrapTable('getData'); + + // How many rows are fully allocated? + var allocated_rows = 0; + + rows.forEach(function(row) { + $(table).bootstrapTable('updateByUniqueId', row.pk, row, true); + + if (isRowFullyAllocated(row)) { + allocated_rows += 1; + } + }); + } } } ); } - console.log("rendering table for output:", outputId); - console.log("allocations:", allocated_items); + if (allocated_items == null) { + + // No allocation data provided? Request from server (blocking) + reloadAllocationData(false); + } function reloadTable() { // Reload the entire build allocation table @@ -1254,6 +1278,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { } function availableQuantity(row) { + // Return the total available stock for a given row // Base stock var available = row.available_stock; @@ -1327,7 +1352,8 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { { source_location: buildInfo.source_location, success: function(data) { - $(table).bootstrapTable('refresh'); + // $(table).bootstrapTable('refresh'); + reloadAllocationData(); }, output: output == null ? null : output.pk, } @@ -1374,6 +1400,9 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { bom_item: row.pk, output: outputId == 'untracked' ? null : outputId, table: table, + onSuccess: function(response, opts) { + reloadAllocationData(); + } }); }); } From df9a33935def45d7a21406209e60713363a3d75f Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Thu, 28 Apr 2022 23:26:08 +1000 Subject: [PATCH 31/47] Row button fixes --- InvenTree/templates/js/translated/build.js | 53 +++++++++++----------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 7705bfaa16..3376cb21a0 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -838,6 +838,23 @@ function loadBuildOutputTable(build_info, options={}) { // List of "tracked bom items" required for this build order var bom_items = null; + // Request list of BOM data for this build order + inventreeGet( + '{% url "api-bom-list" %}', + { + part: build_info.part, + sub_part_detail: true, + sub_part_trackable: true, + }, + { + async: false, + success: function(response) { + // Save the BOM items + bom_items = response; + } + } + ); + /* * Construct a "sub table" showing the required BOM items */ @@ -868,29 +885,6 @@ function loadBuildOutputTable(build_info, options={}) { function updateAllocationData(rows) { // Update stock allocation information for the build outputs - // Request list of BOM data for this build order - if (bom_items == null) { - inventreeGet( - '{% url "api-bom-list" %}', - { - part: build_info.part, - sub_part_detail: true, - sub_part_trackable: true, - }, - { - success: function(response) { - // Save the BOM items - bom_items = response; - - // Callback to this function again - updateAllocationData(rows); - } - } - ); - - return; - } - // Request updated stock allocation data for this build order inventreeGet( '{% url "api-build-item-list" %}', @@ -1098,6 +1092,13 @@ function loadBuildOutputTable(build_info, options={}) { visible: true, switchable: false, formatter: function(value, row) { + // Display a progress bar which shows how many rows have been allocated + var n_bom_lines = 0; + + if (bom_items) { + n_bom_lines = bom_items.length; + } + return `lines: ${n_bom_lines}`; return `<div id='output-progress-${row.pk}'><span class='fas fa-spin fa-spinner'></span></div>`; }, sorter: function(value_a, value_b, row_a, row_b) { @@ -1548,7 +1549,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { quantity: {}, }, title: '{% trans "Edit Allocation" %}', - onSuccess: reloadTable, + onSuccess: reloadAllocationData, }); }); @@ -1558,7 +1559,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { constructForm(`/api/build/item/${pk}/`, { method: 'DELETE', title: '{% trans "Remove Allocation" %}', - onSuccess: reloadTable, + onSuccess: reloadAllocationData, }); }); }, @@ -1718,7 +1719,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { 'fa-minus-circle icon-red', 'button-unallocate', row.sub_part, '{% trans "Unallocate stock" %}', { - disabled: row.allocations == null + disabled: sumAllocations(row) == 0, } ); From 6c6ebe70c27a940f22bcfaa89fbb94fac60846f4 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Fri, 29 Apr 2022 00:27:27 +1000 Subject: [PATCH 32/47] Update progress bars for build output allocation --- InvenTree/templates/js/translated/build.js | 227 ++++++++++++------- InvenTree/templates/js/translated/helpers.js | 40 ++-- 2 files changed, 171 insertions(+), 96 deletions(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 3376cb21a0..2c0b56c518 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -726,6 +726,35 @@ function loadBuildOrderAllocationTable(table, options={}) { } +/* Internal helper functions for performing calculations on BOM data */ + +// Iterate through a list of allocations, returning *only* those which match a particular BOM row +function getAllocationsForBomRow(bom_row, allocations) { + var part_id = bom_row.sub_part; + + var matching_allocations = []; + + allocations.forEach(function(allocation) { + if (allocation.bom_part == part_id) { + matching_allocations.push(allocation); + } + }); + + return matching_allocations; +} + +// Sum the allocation quantity for a given BOM row +function sumAllocationsForBomRow(bom_row, allocations) { + var quantity = 0; + + getAllocationsForBomRow(bom_row, allocations).forEach(function(allocation) { + quantity += allocation.quantity; + }); + + return parseFloat(quantity).toFixed(15); +} + + /* * Display a "build output" table for a particular build. * @@ -919,6 +948,32 @@ function loadBuildOutputTable(build_info, options={}) { rows.forEach(function(row) { row.allocations = allocations[row.pk] || []; $(table).bootstrapTable('updateByUniqueId', row.pk, row, true); + + var n_completed_lines = 0; + + // Check how many BOM lines have been completely allocated for this build output + bom_items.forEach(function(bom_item) { + + var required_quantity = bom_item.quantity * row.quantity; + + if (sumAllocationsForBomRow(bom_item, row.allocations) >= required_quantity) { + n_completed_lines += 1; + } + + var output_progress_bar = $(`#output-progress-${row.pk}`); + + if (output_progress_bar.exists()) { + output_progress_bar.html( + makeProgressBar( + n_completed_lines, + bom_items.length, + { + max_width: '150px', + } + ) + ); + } + }); }); } } @@ -1092,14 +1147,33 @@ function loadBuildOutputTable(build_info, options={}) { visible: true, switchable: false, formatter: function(value, row) { - // Display a progress bar which shows how many rows have been allocated - var n_bom_lines = 0; + + // Display a progress bar which shows how many BOM lines have been fully allocated + var n_bom_lines = 1; + var n_completed_lines = 0; - if (bom_items) { + // Work out how many lines have been allocated for this build output + if (bom_items && row.allocations) { n_bom_lines = bom_items.length; + + bom_items.forEach(function(bom_row) { + var required_quantity = row.quantity * bom_row.quantity; + + if (sumAllocationsForBomRow(bom_row, row.allocations) >= required_quantity) { + n_completed_lines += 1; + } + }) } - return `lines: ${n_bom_lines}`; - return `<div id='output-progress-${row.pk}'><span class='fas fa-spin fa-spinner'></span></div>`; + + var progressBar = makeProgressBar( + n_completed_lines, + n_bom_lines, + { + max_width: '150px', + } + ); + + return `<div id='output-progress-${row.pk}'>${progressBar}</div>`; }, sorter: function(value_a, value_b, row_a, row_b) { // TODO: Custom sorter for "allocated stock" column @@ -1108,7 +1182,7 @@ function loadBuildOutputTable(build_info, options={}) { }, { field: 'tests', - title: '{% trans "Tests" %}', + title: '{% trans "Completed Tests" %}', sortable: true, switchable: true, formatter: function(value, row) { @@ -1186,6 +1260,26 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { outputId = 'untracked'; } + var bom_items = buildInfo.bom_items || null; + + // If BOM items have not been provided, load via the API + if (bom_items == null) { + inventreeGet( + '{% url "api-bom-list" %}', + { + part: partId, + sub_part_detail: true, + sub_part_trackable: trackable, + }, + { + async: false, + success: function(results) { + bom_items = results; + } + } + ); + } + var table = options.table; if (options.table == null) { @@ -1209,6 +1303,42 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { var allocated_items = output == null ? null : output.allocations; + function redrawAllocationData() { + // Force a refresh of each row in the table + // Note we cannot call 'refresh' because we are passing data from memory + // var rows = $(table).bootstrapTable('getData'); + + // How many rows are fully allocated? + var allocated_rows = 0; + + bom_items.forEach(function(row) { + $(table).bootstrapTable('updateByUniqueId', row.pk, row, true); + + if (isRowFullyAllocated(row)) { + allocated_rows += 1; + } + }); + + // Find the top-level progess bar for this build output + var output_progress_bar = $(`#output-progress-${outputId}`); + + if (output_progress_bar.exists()) { + if (bom_items.length > 0) { + output_progress_bar.html( + makeProgressBar( + allocated_rows, + bom_items.length, + { + max_width: '150px', + } + ) + ); + } + } else { + console.warn(`Could not find progress bar for output '${outputId}'`); + } + } + function reloadAllocationData(async=true) { // Reload stock allocation data for this particular build output @@ -1225,32 +1355,18 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { success: function(response) { allocated_items = response; - if (async) { + redrawAllocationData(); - // Force a refresh of each row in the table - // Note we cannot call 'refresh' because we are passing data from memory - var rows = $(table).bootstrapTable('getData'); - - // How many rows are fully allocated? - var allocated_rows = 0; - - rows.forEach(function(row) { - $(table).bootstrapTable('updateByUniqueId', row.pk, row, true); - - if (isRowFullyAllocated(row)) { - allocated_rows += 1; - } - }); - } } } ); } if (allocated_items == null) { - // No allocation data provided? Request from server (blocking) reloadAllocationData(false); + } else { + redrawAllocationData(); } function reloadTable() { @@ -1293,38 +1409,15 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { } return available; - } - function getAllocationsForRow(row) { - var part_id = row.sub_part; - - var allocations = []; - - allocated_items.forEach(function(allocation) { - if (allocation.bom_part == part_id) { - allocations.push(allocation); - } - }); - - return allocations; - } - - function sumAllocations(row) { - - var allocated_quantity = 0; - - getAllocationsForRow(row).forEach(function(allocation) { - allocated_quantity += allocation.quantity; - }); - - row.allocated = parseFloat(allocated_quantity.toFixed(15)); - + function allocatedQuantity(row) { + row.allocated = sumAllocationsForBomRow(row, allocated_items); return row.allocated; } function isRowFullyAllocated(row) { - return sumAllocations(row) >= requiredQuantity(row); + return allocatedQuantity(row) >= requiredQuantity(row); } function setupCallbacks() { @@ -1386,7 +1479,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { newBuildOrder({ part: pk, parent: buildId, - quantity: requiredQuantity(row) - sumAllocations(row), + quantity: requiredQuantity(row) - allocatedQuantity(row), }); }); @@ -1408,26 +1501,6 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { }); } - var bom_items = buildInfo.bom_items || null; - - // If BOM items have not been provided, load via the API - if (bom_items == null) { - inventreeGet( - '{% url "api-bom-list" %}', - { - part: partId, - sub_part_detail: true, - sub_part_trackable: trackable, - }, - { - async: false, - success: function(results) { - bom_items = results; - } - } - ); - } - // Load table of BOM items $(table).inventreeTable({ data: bom_items, @@ -1446,7 +1519,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { showColumns: false, detailView: true, detailFilter: function(index, row) { - return sumAllocations(row) > 0; + return allocatedQuantity(row) > 0; }, detailFormatter: function(index, row, element) { // Contruct an 'inner table' which shows which stock items have been allocated @@ -1460,7 +1533,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { var subTable = $(`#${subTableId}`); subTable.bootstrapTable({ - data: getAllocationsForRow(row), + data: getAllocationsForBomRow(row, allocated_items), showHeader: true, columns: [ { @@ -1660,15 +1733,15 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { title: '{% trans "Allocated" %}', sortable: true, formatter: function(value, row) { - var allocated = sumAllocations(row); + var allocated = allocatedQuantity(row); var required = requiredQuantity(row) return makeProgressBar(allocated, required); }, sorter: function(valA, valB, rowA, rowB) { // Custom sorting function for progress bars - var aA = sumAllocations(rowA); - var aB = sumAllocations(rowB); + var aA = allocatedQuantity(rowA); + var aB = allocatedQuantity(rowB); var qA = requiredQuantity(rowA); var qB = requiredQuantity(rowB); @@ -1703,7 +1776,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { // Generate action buttons for this build output var html = `<div class='btn-group float-right' role='group'>`; - if (sumAllocations(row) < requiredQuantity(row)) { + if (allocatedQuantity(row) < requiredQuantity(row)) { if (row.sub_part_detail.assembly) { html += makeIconButton('fa-tools icon-blue', 'button-build', row.sub_part, '{% trans "Build stock" %}'); } @@ -1719,7 +1792,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { 'fa-minus-circle icon-red', 'button-unallocate', row.sub_part, '{% trans "Unallocate stock" %}', { - disabled: sumAllocations(row) == 0, + disabled: allocatedQuantity(row) == 0, } ); diff --git a/InvenTree/templates/js/translated/helpers.js b/InvenTree/templates/js/translated/helpers.js index c464ad3645..dc40d1e30c 100644 --- a/InvenTree/templates/js/translated/helpers.js +++ b/InvenTree/templates/js/translated/helpers.js @@ -163,27 +163,29 @@ function makeProgressBar(value, maximum, opts={}) { var style = options.style || ''; - var text = ''; + var text = options.text; + + if (!text) { + if (style == 'percent') { + // Display e.g. "50%" - if (style == 'percent') { - // Display e.g. "50%" + text = `${percent}%`; + } else if (style == 'max') { + // Display just the maximum value + text = `${maximum}`; + } else if (style == 'value') { + // Display just the current value + text = `${value}`; + } else if (style == 'blank') { + // No display! + text = ''; + } else { + /* Default style + * Display e.g. "5 / 10" + */ - text = `${percent}%`; - } else if (style == 'max') { - // Display just the maximum value - text = `${maximum}`; - } else if (style == 'value') { - // Display just the current value - text = `${value}`; - } else if (style == 'blank') { - // No display! - text = ''; - } else { - /* Default style - * Display e.g. "5 / 10" - */ - - text = `${value} / ${maximum}`; + text = `${value} / ${maximum}`; + } } var id = options.id || 'progress-bar'; From 3da644637380a83e605a20cc420a7ffcb0f3dfc6 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Fri, 29 Apr 2022 00:32:33 +1000 Subject: [PATCH 33/47] Allow sorting of build output table by allocated items --- InvenTree/templates/js/translated/build.js | 43 +++++++++++----------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 2c0b56c518..c91dc1ccc0 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -1060,6 +1060,21 @@ function loadBuildOutputTable(build_info, options={}) { return n; } + // Return the number of 'fully allocated' lines for a given row + function countAllocatedLines(row) { + var n_completed_lines = 0; + + bom_items.forEach(function(bom_row) { + var required_quantity = bom_row.quantity * row.quantity; + + if (sumAllocationsForBomRow(bom_row, row.allocations || []) >= required_quantity) { + n_completed_lines += 1; + } + }); + + return n_completed_lines; + } + $(table).inventreeTable({ url: '{% url "api-stock-list" %}', queryParams: filters, @@ -1146,28 +1161,12 @@ function loadBuildOutputTable(build_info, options={}) { title: '{% trans "Allocated Stock" %}', visible: true, switchable: false, + sortable: true, formatter: function(value, row) { - // Display a progress bar which shows how many BOM lines have been fully allocated - var n_bom_lines = 1; - var n_completed_lines = 0; - - // Work out how many lines have been allocated for this build output - if (bom_items && row.allocations) { - n_bom_lines = bom_items.length; - - bom_items.forEach(function(bom_row) { - var required_quantity = row.quantity * bom_row.quantity; - - if (sumAllocationsForBomRow(bom_row, row.allocations) >= required_quantity) { - n_completed_lines += 1; - } - }) - } - var progressBar = makeProgressBar( - n_completed_lines, - n_bom_lines, + countAllocatedLines(row), + bom_items.length, { max_width: '150px', } @@ -1176,8 +1175,10 @@ function loadBuildOutputTable(build_info, options={}) { return `<div id='output-progress-${row.pk}'>${progressBar}</div>`; }, sorter: function(value_a, value_b, row_a, row_b) { - // TODO: Custom sorter for "allocated stock" column - return 0; + var q_a = countAllocatedLines(row_a); + var q_b = countAllocatedLines(row_b); + + return q_a > q_b ? 1 : -1; }, }, { From 6e52ca21785c146ec6c1a0df83843b14de0b6090 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Fri, 29 Apr 2022 00:44:29 +1000 Subject: [PATCH 34/47] Refactor button callbacks - Add button to expand all output rows - Add button to collapse all output rows --- InvenTree/build/templates/build/detail.html | 72 +++----------------- InvenTree/templates/js/translated/build.js | 73 +++++++++++++++++++++ 2 files changed, 83 insertions(+), 62 deletions(-) diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index c01c9055f1..52b81e6389 100644 --- a/InvenTree/build/templates/build/detail.html +++ b/InvenTree/build/templates/build/detail.html @@ -270,6 +270,16 @@ </ul> </div> + {% if build.has_tracked_bom_items %} + <button id='outputs-expand' class='btn btn-outline-secondary' type='button' title='{% trans "Expand all build output rows" %}'> + <span class='fas fa-expand'></span> + </button> + + <button id='outputs-collapse' class='btn btn-outline-secondary' type='button' title='{% trans "Collapse all build output rows" %}'> + <span class='fas fa-compress'></span> + </button> + {% endif %} + {% include "filter_list.html" with id='incompletebuilditems' %} </div> {% endif %} @@ -416,68 +426,6 @@ onPanelLoad('outputs', function() { loadBuildOutputTable(build_info); - linkButtonsToSelection( - '#build-output-table', - [ - '#output-options', - '#multi-output-complete', - '#multi-output-delete', - ] - ); - - $('#multi-output-complete').click(function() { - var outputs = $('#build-output-table').bootstrapTable('getSelections'); - - completeBuildOutputs( - build_info.pk, - outputs, - { - success: function() { - // Reload the "in progress" table - $('#build-output-table').bootstrapTable('refresh'); - - // Reload the "completed" table - $('#build-stock-table').bootstrapTable('refresh'); - } - } - ); - }); - - $('#multi-output-delete').click(function() { - var outputs = $('#build-output-table').bootstrapTable('getSelections'); - - deleteBuildOutputs( - build_info.pk, - outputs, - { - success: function() { - // Reload the "in progress" table - $('#build-output-table').bootstrapTable('refresh'); - - // Reload the "completed" table - $('#build-stock-table').bootstrapTable('refresh'); - } - } - ) - }); - - $('#incomplete-output-print-label').click(function() { - var outputs = $('#build-output-table').bootstrapTable('getSelections'); - - if (outputs.length == 0) { - outputs = $('#build-output-table').bootstrapTable('getData'); - } - - var stock_id_values = []; - - outputs.forEach(function(output) { - stock_id_values.push(output.pk); - }); - - printStockItemLabels(stock_id_values); - - }); - {% endif %} }); diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index c91dc1ccc0..935206ce2f 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -1233,6 +1233,79 @@ function loadBuildOutputTable(build_info, options={}) { $(table).on('collapse-row.bs.table', function(detail, index, row) { $(`#button-output-allocate-${row.pk}`).prop('disabled', true); }); + + // Add callbacks for the various table menubar buttons + + // Complete multiple outputs + $('#multi-output-complete').click(function() { + var outputs = $(table).bootstrapTable('getSelections'); + + if (outputs.length == 0) { + outputs = $(table).bootstrapTable('getData'); + } + + completeBuildOutputs( + build_info.pk, + outputs, + { + success: function() { + // Reload the "in progress" table + $('#build-output-table').bootstrapTable('refresh'); + + // Reload the "completed" table + $('#build-stock-table').bootstrapTable('refresh'); + } + } + ); + }); + + // Delete multiple build outputs + $('#multi-output-delete').click(function() { + var outputs = $(table).bootstrapTable('getSelections'); + + if (outputs.length == 0) { + outputs = $(table).bootstrapTable('getData'); + } + + deleteBuildOutputs( + build_info.pk, + outputs, + { + success: function() { + // Reload the "in progress" table + $('#build-output-table').bootstrapTable('refresh'); + + // Reload the "completed" table + $('#build-stock-table').bootstrapTable('refresh'); + } + } + ) + }); + + // Print stock item labels + $('#incomplete-output-print-label').click(function() { + var outputs = $(table).bootstrapTable('getSelections'); + + if (outputs.length == 0) { + outputs = $(table).bootstrapTable('getData'); + } + + var stock_id_values = []; + + outputs.forEach(function(output) { + stock_id_values.push(output.pk); + }); + + printStockItemLabels(stock_id_values); + }); + + $('#outputs-expand').click(function() { + $(table).bootstrapTable('expandAllRows'); + }); + + $('#outputs-collapse').click(function() { + $(table).bootstrapTable('collapseAllRows'); + }); } From 81638d06cf06901bf879c5d336ec4449b9e60443 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Fri, 29 Apr 2022 00:51:56 +1000 Subject: [PATCH 35/47] Show or hide items based on output BOM --- InvenTree/templates/js/translated/build.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 935206ce2f..705060ce63 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -264,7 +264,7 @@ function makeBuildOutputButtons(output_id, build_info, options={}) { var html = `<div class='btn-group float-right' role='group'>`; // Tracked parts? Must be individually allocated - if (build_info.tracked_parts) { + if (options.has_bom_items) { // Add a button to allocate stock against this build output html += makeIconButton( @@ -1085,9 +1085,9 @@ function loadBuildOutputTable(build_info, options={}) { sortable: true, search: false, sidePagination: 'client', - detailView: true, + detailView: bom_items.length > 0, detailFilter: function(index, row) { - return true; + return bom_items.length > 0; }, detailFormatter: function(index, row, element) { constructBuildOutputSubTable(index, row, element); @@ -1159,11 +1159,15 @@ function loadBuildOutputTable(build_info, options={}) { { field: 'allocated', title: '{% trans "Allocated Stock" %}', - visible: true, + visible: bom_items.length > 0, switchable: false, sortable: true, formatter: function(value, row) { + if (bom_items.length == 0) { + return `<div id='output-progress-${row.pk}'><em><small>{% trans "No tracked BOM items for this build" %}</small></em></div>`; + } + var progressBar = makeProgressBar( countAllocatedLines(row), bom_items.length, @@ -1188,7 +1192,7 @@ function loadBuildOutputTable(build_info, options={}) { switchable: true, formatter: function(value, row) { if (part_tests == null || part_tests.length == 0) { - return `<em>{% trans "No tests found " %}</em>`; + return `<em><small>{% trans "No required tests for this build" %}</small></em>`; } var n_passed = countPassedTests(row); @@ -1218,6 +1222,9 @@ function loadBuildOutputTable(build_info, options={}) { return makeBuildOutputButtons( row.pk, build_info, + { + has_bom_items: bom_items.length > 0, + } ); } } From 6b4592b3dcc2349f00a4125ff4cf9a1c6e13d628 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Fri, 29 Apr 2022 01:10:05 +1000 Subject: [PATCH 36/47] Display error if stock item is "double allocted" --- InvenTree/build/serializers.py | 9 +++++++-- InvenTree/templates/js/translated/build.js | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 7df2c474a9..d037ad546e 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -630,6 +630,7 @@ class BuildAllocationItemSerializer(serializers.Serializer): super().validate(data) + build = self.context['build'] bom_item = data['bom_item'] stock_item = data['stock_item'] quantity = data['quantity'] @@ -654,16 +655,20 @@ class BuildAllocationItemSerializer(serializers.Serializer): # Output *must* be set for trackable parts if output is None and bom_item.sub_part.trackable: raise ValidationError({ - 'output': _('Build output must be specified for allocation of tracked parts') + 'output': _('Build output must be specified for allocation of tracked parts'), }) # Output *cannot* be set for un-tracked parts if output is not None and not bom_item.sub_part.trackable: raise ValidationError({ - 'output': _('Build output cannot be specified for allocation of untracked parts') + 'output': _('Build output cannot be specified for allocation of untracked parts'), }) + # Check if this allocation would be unique + if BuildItem.objects.filter(build=build, stock_item=stock_item, install_into=output).exists(): + raise ValidationError(_('This stock item has already been allocated to this build output')) + return data diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 705060ce63..c996501060 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -1982,7 +1982,7 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) { // var stock_input = constructRelatedFieldInput(`items_stock_item_${pk}`); var html = ` - <tr id='allocation_row_${pk}' class='part-allocation-row'> + <tr id='items_${pk}' class='part-allocation-row'> <td id='part_${pk}'> ${thumb} ${sub_part.full_name} </td> @@ -2167,7 +2167,7 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) { $(options.modal).find('.button-row-remove').click(function() { var pk = $(this).attr('pk'); - $(options.modal).find(`#allocation_row_${pk}`).remove(); + $(options.modal).find(`#items_${pk}`).remove(); }); }, onSubmit: function(fields, opts) { From e0189be5a6ec0e92ba8ee67fa63d2a5f0cd1fbea Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Fri, 29 Apr 2022 01:19:36 +1000 Subject: [PATCH 37/47] Adds ability to filter StockItemTestresult API list by Build ID - Allows us to retrieve stock item test results in a single API query --- InvenTree/stock/api.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index d4fc5c93d1..f6b21ca5af 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -23,6 +23,8 @@ from rest_framework.serializers import ValidationError from rest_framework.response import Response from rest_framework import generics, filters +from build.models import Build + import common.settings import common.models @@ -1159,6 +1161,20 @@ class StockItemTestResultList(generics.ListCreateAPIView): queryset = super().filter_queryset(queryset) + # Filter by 'build' + build = params.get('build', None) + + if build is not None: + + try: + build = Build.objects.get(pk=build) + + queryset = queryset.filter(stock_item__build=build) + + except (ValueError, Build.DoesNotExist): + pass + + # Filter by stock item item = params.get('stock_item', None) From a0ca20ab04e71d978b2519dcd557af78962420fb Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Fri, 29 Apr 2022 01:27:58 +1000 Subject: [PATCH 38/47] Retrieve all stock item test results at once --- InvenTree/templates/js/translated/build.js | 45 ++++++++++------------ 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index c996501060..5fe9165c9d 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -1007,39 +1007,36 @@ function loadBuildOutputTable(build_info, options={}) { return; } - rows.forEach(function(row) { + // Retrieve stock results for the entire build + inventreeGet( + '{% url "api-stock-test-result-list" %}', + { + build: build_info.pk, + }, + { + success: function(results) { - // Ignore if this row has already been updated (else, infinite loop!) - if (row.passed_tests) { - return; - } + // Iterate through each row and find matching test results + rows.forEach(function(row) { + var test_results = {}; - // Request test result information for the particular build output - inventreeGet( - '{% url "api-stock-test-result-list" %}', - { - stock_item: row.pk, - }, - { - success: function(results) { - - // A list of tests that this stock item has passed - var passed_tests = {}; - - // Keep a list of tests that this stock item has passed results.forEach(function(result) { - if (result.result) { - passed_tests[result.key] = true; + if (result.stock_item == row.pk) { + // This test result matches the particular stock item + + if (!(result.key in test_results)) { + test_results[result.key] = result.result; + } } }); - row.passed_tests = passed_tests; + row.passed_tests = test_results; $(table).bootstrapTable('updateByUniqueId', row.pk, row, true); - } + }); } - ) - }); + } + ); } // Return the number of 'passed' tests in a given row From 0bda9c974e1389a8ef08bdcb1fab9db1fab5e9fa Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Fri, 29 Apr 2022 01:35:09 +1000 Subject: [PATCH 39/47] PEP fixes --- InvenTree/build/models.py | 2 +- InvenTree/stock/api.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 61783fc8fb..86bb256539 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -1292,7 +1292,7 @@ class BuildItem(models.Model): user, notes ) - + else: # Simply remove the items from stock item.take_stock( diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index f6b21ca5af..c88c29e64e 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -1165,7 +1165,7 @@ class StockItemTestResultList(generics.ListCreateAPIView): build = params.get('build', None) if build is not None: - + try: build = Build.objects.get(pk=build) @@ -1174,7 +1174,6 @@ class StockItemTestResultList(generics.ListCreateAPIView): except (ValueError, Build.DoesNotExist): pass - # Filter by stock item item = params.get('stock_item', None) From b595fa0e7eb47d734d3d2c35cffb65e18e72d425 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Fri, 29 Apr 2022 01:40:59 +1000 Subject: [PATCH 40/47] Fix loading of untracked parts table --- InvenTree/templates/js/translated/build.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 5fe9165c9d..502f95d65f 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -1347,7 +1347,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { { part: partId, sub_part_detail: true, - sub_part_trackable: trackable, + sub_part_trackable: buildInfo.tracked_parts, }, { async: false, From 51da1f02a8d126e32fa84da7f1057ad52f13afb4 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Fri, 29 Apr 2022 01:58:57 +1000 Subject: [PATCH 41/47] JS linting fixes --- InvenTree/templates/js/translated/build.js | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 502f95d65f..a88f3ccb28 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -867,7 +867,7 @@ function loadBuildOutputTable(build_info, options={}) { // List of "tracked bom items" required for this build order var bom_items = null; - // Request list of BOM data for this build order + // Request list of BOM data for this build order inventreeGet( '{% url "api-bom-list" %}', { @@ -962,7 +962,7 @@ function loadBuildOutputTable(build_info, options={}) { var output_progress_bar = $(`#output-progress-${row.pk}`); - if (output_progress_bar.exists()) { + if (output_progress_bar.exists()) { output_progress_bar.html( makeProgressBar( n_completed_lines, @@ -977,7 +977,7 @@ function loadBuildOutputTable(build_info, options={}) { }); } } - ) + ); } var part_tests = null; @@ -1283,14 +1283,14 @@ function loadBuildOutputTable(build_info, options={}) { $('#build-stock-table').bootstrapTable('refresh'); } } - ) + ); }); // Print stock item labels $('#incomplete-output-print-label').click(function() { var outputs = $(table).bootstrapTable('getSelections'); - if (outputs.length == 0) { + if (outputs.length == 0) { outputs = $(table).bootstrapTable('getData'); } @@ -1375,10 +1375,6 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { setupFilterList('builditems', $(table), options.filterTarget); - // If an "output" is specified, then only "trackable" parts are allocated - // Otherwise, only "untrackable" parts are allowed - var trackable = ! !output; - var allocated_items = output == null ? null : output.allocations; function redrawAllocationData() { @@ -1447,11 +1443,6 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { redrawAllocationData(); } - function reloadTable() { - // Reload the entire build allocation table - $(table).bootstrapTable('refresh'); - } - function requiredQuantity(row) { // Return the requied quantity for a given row @@ -1812,7 +1803,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { sortable: true, formatter: function(value, row) { var allocated = allocatedQuantity(row); - var required = requiredQuantity(row) + var required = requiredQuantity(row); return makeProgressBar(allocated, required); }, sorter: function(valA, valB, rowA, rowB) { From 8fc34a21a6b7be239c00743ce3e14a91ecd52669 Mon Sep 17 00:00:00 2001 From: Oliver <oliver.henry.walters@gmail.com> Date: Fri, 29 Apr 2022 12:59:20 +1000 Subject: [PATCH 42/47] Reload the untracked stock table when allocation actions are performed --- InvenTree/build/templates/build/detail.html | 52 ++++++++++++--------- InvenTree/templates/js/translated/build.js | 4 +- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index 52b81e6389..42bc51bb2f 100644 --- a/InvenTree/build/templates/build/detail.html +++ b/InvenTree/build/templates/build/detail.html @@ -431,24 +431,32 @@ onPanelLoad('outputs', function() { {% if build.active and build.has_untracked_bom_items %} -var build_info = { - pk: {{ build.pk }}, - part: {{ build.part.pk }}, - quantity: {{ build.quantity }}, - {% if build.take_from %} - source_location: {{ build.take_from.pk }}, - {% endif %} - tracked_parts: false, -}; +function loadUntrackedStockTable() { + + var build_info = { + pk: {{ build.pk }}, + part: {{ build.part.pk }}, + quantity: {{ build.quantity }}, + {% if build.take_from %} + source_location: {{ build.take_from.pk }}, + {% endif %} + tracked_parts: false, + }; + + $('#allocation-table-untracked').bootstrapTable('destroy'); + + // Load allocation table for un-tracked parts + loadBuildOutputAllocationTable( + build_info, + null, + { + search: true, + } + ); +} + +loadUntrackedStockTable(); -// Load allocation table for un-tracked parts -loadBuildOutputAllocationTable( - build_info, - null, - { - search: true, - } -); {% endif %} $('#btn-create-output').click(function() { @@ -472,6 +480,7 @@ $("#btn-auto-allocate").on('click', function() { {% if build.take_from %} location: {{ build.take_from.pk }}, {% endif %} + onSuccess: loadUntrackedStockTable, } ); }); @@ -503,9 +512,7 @@ $("#btn-allocate").on('click', function() { {% if build.take_from %} source_location: {{ build.take_from.pk }}, {% endif %} - success: function(data) { - $('#allocation-table-untracked').bootstrapTable('refresh'); - } + success: loadUntrackedStockTable, } ); } @@ -514,6 +521,7 @@ $("#btn-allocate").on('click', function() { $('#btn-unallocate').on('click', function() { unallocateStock({{ build.id }}, { table: '#allocation-table-untracked', + onSuccess: loadUntrackedStockTable, }); }); @@ -533,9 +541,7 @@ $('#allocate-selected-items').click(function() { {% if build.take_from %} source_location: {{ build.take_from.pk }}, {% endif %} - success: function(data) { - $('#allocation-table-untracked').bootstrapTable('refresh'); - } + success: loadUntrackedStockTable, } ); }); diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index a88f3ccb28..a8a973d516 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -2270,7 +2270,9 @@ function autoAllocateStockToBuild(build_id, bom_items=[], options={}) { confirm: true, preFormContent: html, onSuccess: function(response) { - $('#allocation-table-untracked').bootstrapTable('refresh'); + if (options.onSuccess) { + options.onSuccess(response); + } } }); } From a80465e85cbdab6093479c97c18a7bb04a547d0c Mon Sep 17 00:00:00 2001 From: Oliver <oliver.henry.walters@gmail.com> Date: Fri, 29 Apr 2022 13:03:41 +1000 Subject: [PATCH 43/47] Display batch code in build output table --- InvenTree/templates/js/translated/build.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index a8a973d516..719df6ba5c 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -1135,6 +1135,10 @@ function loadBuildOutputTable(build_info, options={}) { text = `{% trans "Quantity" %}: ${row.quantity}`; } + if (row.batch) { + text += ` <small>({% trans "Batch" %}: ${row.batch})</small>`; + } + return renderLink(text, url); }, sorter: function(a, b, row_a, row_b) { From 94fa424440b033fc306c6bbc1db64fb0606b7ff6 Mon Sep 17 00:00:00 2001 From: Oliver <oliver.henry.walters@gmail.com> Date: Fri, 29 Apr 2022 13:13:12 +1000 Subject: [PATCH 44/47] Table tweaks --- InvenTree/build/api.py | 1 + InvenTree/templates/js/translated/build.js | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index 22dd6473ab..a720f7cbe0 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -96,6 +96,7 @@ class BuildList(generics.ListCreateAPIView): 'target_date', 'completion_date', 'quantity', + 'completed', 'issued_by', 'responsible', ] diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 719df6ba5c..c9ebbe0e22 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -1121,7 +1121,7 @@ function loadBuildOutputTable(build_info, options={}) { { field: 'quantity', title: '{% trans "Build Output" %}', - switchable: true, + switchable: false, sortable: true, formatter: function(value, row) { @@ -1834,12 +1834,12 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { // Handle the case where both ratios are equal if (progressA == progressB) { - return (qA < qB) ? 1 : -1; + return (qA > qB) ? 1 : -1; } if (progressA == progressB) return 0; - return (progressA < progressB) ? 1 : -1; + return (progressA > progressB) ? 1 : -1; } }, { @@ -2374,8 +2374,8 @@ function loadBuildTable(table, options) { } }, { - field: 'quantity', - title: '{% trans "Completed" %}', + field: 'completed', + title: '{% trans "Progress" %}', sortable: true, formatter: function(value, row) { return makeProgressBar( From 1bb2551edb2afff7543c86cdc0566bd9c5fb8521 Mon Sep 17 00:00:00 2001 From: Oliver <oliver.henry.walters@gmail.com> Date: Fri, 29 Apr 2022 13:51:49 +1000 Subject: [PATCH 45/47] Fixes for model rendering code --- .../js/translated/model_renderers.js | 26 +++++++------------ 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/InvenTree/templates/js/translated/model_renderers.js b/InvenTree/templates/js/translated/model_renderers.js index 656a3f9f63..b88de5af35 100644 --- a/InvenTree/templates/js/translated/model_renderers.js +++ b/InvenTree/templates/js/translated/model_renderers.js @@ -113,8 +113,6 @@ function renderStockItem(name, data, parameters={}, options={}) { } } - - var html = ` <span> ${part_detail} @@ -146,7 +144,7 @@ function renderStockLocation(name, data, parameters={}, options={}) { html += ` - <i>${data.description}</i>`; } - html += `<span class='float-right'><small>{% trans "Location ID" %}: ${data.pk}</small></span>`; + html += renderId('{% trans "Location ID" %}', data.pk, parameters); return html; } @@ -162,10 +160,9 @@ function renderBuild(name, data, parameters={}, options={}) { var html = select2Thumbnail(image); - html += `<span><b>${data.reference}</b></span> - ${data.quantity} x ${data.part_detail.full_name}`; - html += `<span class='float-right'><small>{% trans "Build ID" %}: ${data.pk}</span></span>`; + html += `<span><b>${data.reference}</b> - ${data.quantity} x ${data.part_detail.full_name}</span>`; - html += `<p><i>${data.title}</i></p>`; + html += renderId('{% trans "Build ID" %}', data.pk, parameters); return html; } @@ -300,12 +297,9 @@ function renderSalesOrderShipment(name, data, parameters={}, options={}) { var so_prefix = global_settings.SALESORDER_REFERENCE_PREFIX; - var html = ` - <span>${so_prefix}${data.order_detail.reference} - {% trans "Shipment" %} ${data.reference}</span> - <span class='float-right'> - <small>{% trans "Shipment ID" %}: ${data.pk}</small> - </span> - `; + var html = `<span>${so_prefix}${data.order_detail.reference} - {% trans "Shipment" %} ${data.reference}</span>`; + + html += renderId('{% trans "Shipment ID" %}', data.pk, parameters); return html; } @@ -323,7 +317,7 @@ function renderPartCategory(name, data, parameters={}, options={}) { html += ` - <i>${data.description}</i>`; } - html += `<span class='float-right'><small>{% trans "Category ID" %}: ${data.pk}</small></span>`; + html += renderId('{% trans "Category ID" %}', data.pk, parameters); return html; } @@ -366,7 +360,7 @@ function renderManufacturerPart(name, data, parameters={}, options={}) { html += ` <span><b>${data.manufacturer_detail.name}</b> - ${data.MPN}</span>`; html += ` - <i>${data.part_detail.full_name}</i>`; - html += `<span class='float-right'><small>{% trans "Manufacturer Part ID" %}: ${data.pk}</small></span>`; + html += renderId('{% trans "Manufacturer Part ID" %}', data.pk, parameters); return html; } @@ -395,9 +389,7 @@ function renderSupplierPart(name, data, parameters={}, options={}) { html += ` <span><b>${data.supplier_detail.name}</b> - ${data.SKU}</span>`; html += ` - <i>${data.part_detail.full_name}</i>`; - html += `<span class='float-right'><small>{% trans "Supplier Part ID" %}: ${data.pk}</small></span>`; - + html += renderId('{% trans "Supplier Part ID" %}', data.pk, parameters); return html; - } From 260680c5c494156dcb628618e461964f83e04580 Mon Sep 17 00:00:00 2001 From: Oliver <oliver.henry.walters@gmail.com> Date: Fri, 29 Apr 2022 17:07:54 +1000 Subject: [PATCH 46/47] Refactor BOM table to not load multi level BOMs by default - Click to select which ones to load --- InvenTree/templates/js/translated/bom.js | 86 +++++++++++------------- 1 file changed, 39 insertions(+), 47 deletions(-) diff --git a/InvenTree/templates/js/translated/bom.js b/InvenTree/templates/js/translated/bom.js index 2d7796edcd..c3180bf490 100644 --- a/InvenTree/templates/js/translated/bom.js +++ b/InvenTree/templates/js/translated/bom.js @@ -743,11 +743,29 @@ function loadBomTable(table, options={}) { field: 'sub_part', title: '{% trans "Part" %}', sortable: true, + switchable: false, formatter: function(value, row) { var url = `/part/${row.sub_part}/`; - var html = imageHoverIcon(row.sub_part_detail.thumbnail) + renderLink(row.sub_part_detail.full_name, url); + var html = ''; var sub_part = row.sub_part_detail; + + // Display an extra icon if this part is an assembly + if (sub_part.assembly) { + + if (row.sub_assembly_received) { + // Data received, ignore + } else if (row.sub_assembly_requested) { + html += `<a href='#'><span class='fas fa-sync fa-spin'></span></a>`; + } else { + html += ` + <a href='#' pk='${row.pk}' class='load-sub-assembly' id='load-sub-assembly-${row.pk}'> + <span class='fas fa-sync-alt' title='{% trans "Load BOM for subassembly" %}'></span> + </a> `; + } + } + + html += imageHoverIcon(sub_part.thumbnail) + renderLink(row.sub_part_detail.full_name, url); html += makePartIcons(sub_part); @@ -759,13 +777,6 @@ function loadBomTable(table, options={}) { html += makeIconBadge('fa-sitemap', '{% trans "Variant stock allowed" %}'); } - // Display an extra icon if this part is an assembly - if (sub_part.assembly) { - var text = `<span title='{% trans "Open subassembly" %}' class='fas fa-stream float-right'></span>`; - - html += renderLink(text, `/part/${row.sub_part}/bom/`); - } - return html; } } @@ -1027,14 +1038,6 @@ function loadBomTable(table, options={}) { // This function may be called recursively for multi-level BOMs function requestSubItems(bom_pk, part_pk, depth=0) { - // Prevent multi-level recursion - const MAX_BOM_DEPTH = 25; - - if (depth >= MAX_BOM_DEPTH) { - console.log(`Maximum BOM depth (${MAX_BOM_DEPTH}) reached!`); - return; - } - inventreeGet( options.bom_url, { @@ -1049,17 +1052,13 @@ function loadBomTable(table, options={}) { for (var idx = 0; idx < response.length; idx++) { response[idx].parentId = bom_pk; } + + var row = $(table).bootstrapTable('getRowByUniqueId', bom_pk); + row.sub_assembly_received = true; + + $(table).bootstrapTable('updateByUniqueId', bom_pk, row, true); table.bootstrapTable('append', response); - - // Next, re-iterate and check if the new items also have sub items - response.forEach(function(bom_item) { - if (bom_item.sub_part_detail.assembly) { - requestSubItems(bom_item.pk, bom_item.sub_part, depth + 1); - } - }); - - table.treegrid('collapseAll'); }, error: function(xhr) { console.log('Error requesting BOM for part=' + part_pk); @@ -1103,7 +1102,6 @@ function loadBomTable(table, options={}) { formatNoMatches: function() { return '{% trans "No BOM items found" %}'; }, - clickToSelect: true, queryParams: filters, original: params, columns: cols, @@ -1117,32 +1115,26 @@ function loadBomTable(table, options={}) { }); table.treegrid('collapseAll'); + + // Callback for 'load sub assembly' button + $(table).find('.load-sub-assembly').click(function(event) { + + event.preventDefault(); + + var pk = $(this).attr('pk'); + var row = $(table).bootstrapTable('getRowByUniqueId', pk); + + // Request BOM data for this subassembly + requestSubItems(row.pk, row.sub_part); + + row.sub_assembly_requested = true; + $(table).bootstrapTable('updateByUniqueId', pk, row, true); + }); }, onLoadSuccess: function() { - if (options.editable) { table.bootstrapTable('uncheckAll'); } - - var data = table.bootstrapTable('getData'); - - for (var idx = 0; idx < data.length; idx++) { - var row = data[idx]; - - // If a row already has a parent ID set, it's already been updated! - if (row.parentId) { - continue; - } - - // Set the parent ID of the top-level rows - row.parentId = parent_id; - - table.bootstrapTable('updateRow', idx, row, true); - - if (row.sub_part_detail.assembly) { - requestSubItems(row.pk, row.sub_part); - } - } }, }); From 35de490f72b75b28ffdfee3a39295f145e87adf5 Mon Sep 17 00:00:00 2001 From: Oliver <oliver.henry.walters@gmail.com> Date: Fri, 29 Apr 2022 17:13:59 +1000 Subject: [PATCH 47/47] JS linting fixes --- InvenTree/templates/js/translated/bom.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/templates/js/translated/bom.js b/InvenTree/templates/js/translated/bom.js index c3180bf490..e4674d5989 100644 --- a/InvenTree/templates/js/translated/bom.js +++ b/InvenTree/templates/js/translated/bom.js @@ -1116,8 +1116,8 @@ function loadBomTable(table, options={}) { table.treegrid('collapseAll'); - // Callback for 'load sub assembly' button - $(table).find('.load-sub-assembly').click(function(event) { + // Callback for 'load sub assembly' button + $(table).find('.load-sub-assembly').click(function(event) { event.preventDefault();