From 549f16b7aa27b2cc28f0db748349907c4f2f6a25 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 3 Mar 2022 13:53:09 +1100 Subject: [PATCH 01/12] Adds "export" option to StockItem API endpoint, allowing export to file --- InvenTree/stock/api.py | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index ed7a4b8c40..f60f1c535f 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -30,6 +30,7 @@ from company.models import Company, SupplierPart from company.serializers import CompanySerializer, SupplierPartSerializer from InvenTree.helpers import str2bool, isNull, extract_serial_numbers +from InvenTree.helpers import DownloadFile from InvenTree.api import AttachmentMixin from InvenTree.filters import InvenTreeOrderingFilter @@ -40,6 +41,7 @@ from order.serializers import POSerializer from part.models import BomItem, Part, PartCategory from part.serializers import PartBriefSerializer +from stock.admin import StockItemResource from stock.models import StockLocation, StockItem from stock.models import StockItemTracking from stock.models import StockItemAttachment @@ -611,6 +613,27 @@ class StockList(generics.ListCreateAPIView): queryset = self.filter_queryset(self.get_queryset()) + params = request.query_params + + # Check if we wish to export the queried data to a file. + # If so, skip pagination! + export_format = params.get('export', None) + + if export_format: + export_format = str(export_format).strip().lower() + + if export_format in ['csv', 'tsv', 'xls', 'xlsx']: + dataset = StockItemResource().export(queryset=queryset) + + filedata = dataset.export(export_format) + + filename = 'InvenTree_Stocktake_{date}.{fmt}'.format( + date=datetime.now().strftime("%d-%b-%Y"), + fmt=export_format + ) + + return DownloadFile(filedata, filename) + page = self.paginate_queryset(queryset) if page is not None: @@ -641,7 +664,7 @@ class StockList(generics.ListCreateAPIView): supplier_part_ids.add(sp) # Do we wish to include Part detail? - if str2bool(request.query_params.get('part_detail', False)): + if str2bool(params.get('part_detail', False)): # Fetch only the required Part objects from the database parts = Part.objects.filter(pk__in=part_ids).prefetch_related( @@ -659,7 +682,7 @@ class StockList(generics.ListCreateAPIView): stock_item['part_detail'] = part_map.get(part_id, None) # Do we wish to include SupplierPart detail? - if str2bool(request.query_params.get('supplier_part_detail', False)): + if str2bool(params.get('supplier_part_detail', False)): supplier_parts = SupplierPart.objects.filter(pk__in=supplier_part_ids) @@ -673,7 +696,7 @@ class StockList(generics.ListCreateAPIView): stock_item['supplier_part_detail'] = supplier_part_map.get(part_id, None) # Do we wish to include StockLocation detail? - if str2bool(request.query_params.get('location_detail', False)): + if str2bool(params.get('location_detail', False)): # Fetch only the required StockLocation objects from the database locations = StockLocation.objects.filter(pk__in=location_ids).prefetch_related( From 6b7a0fde1bc4afca1f8484d29a9c00ffdae6fe4a Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 3 Mar 2022 17:17:27 +1100 Subject: [PATCH 02/12] Store table query parameters when performing a bootstrap-table query - For now it only supports .csv format --- InvenTree/stock/templates/stock/location.html | 7 +-- InvenTree/templates/js/translated/tables.js | 45 +++++++++++++++++++ 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/InvenTree/stock/templates/stock/location.html b/InvenTree/stock/templates/stock/location.html index 4c98db529b..575a798fb2 100644 --- a/InvenTree/stock/templates/stock/location.html +++ b/InvenTree/stock/templates/stock/location.html @@ -240,12 +240,7 @@ {% endif %} $("#stock-export").click(function() { - - exportStock({ - {% if location %} - location: {{ location.pk }} - {% endif %} - }); + downloadTableData($('#stock-table')); }); $('#location-create').click(function () { diff --git a/InvenTree/templates/js/translated/tables.js b/InvenTree/templates/js/translated/tables.js index c2418dbe78..b295c3f89a 100644 --- a/InvenTree/templates/js/translated/tables.js +++ b/InvenTree/templates/js/translated/tables.js @@ -7,6 +7,7 @@ /* exported customGroupSorter, + downloadTableData, reloadtable, renderLink, reloadTableFilters, @@ -21,6 +22,42 @@ function reloadtable(table) { } +/** + * Download data from a table, via the API. + * This requires a number of conditions to be met: + * + * - The API endpoint supports data download (on the server side) + * - The table is "flat" (does not support multi-level loading, etc) + * - The table has been loaded using the inventreeTable() function, not bootstrapTable() + * (Refer to the "reloadTableFilters" function to see why!) + */ +function downloadTableData(table) { + + // Extract table configuration options + var options = table.bootstrapTable('getOptions'); + + var url = options.url; + + if (!url) { + console.log("Error: downloadTableData could not find 'url' parameter"); + } + + var query_params = options.query_params || {}; + + url += '?'; + + for (const [key, value] of Object.entries(query_params)) { + url += `${key}=${value}&`; + } + + var format = 'csv'; + + url += `export=${format}`; + + location.href = url; +} + + /** * Render a URL for display * @param {String} text @@ -114,6 +151,10 @@ function reloadTableFilters(table, filters) { } } + // Store the total set of query params + // This is necessary for the "downloadTableData" function to work + options.query_params = params; + options.queryParams = function(tableParams) { return convertQueryParameters(tableParams, params); }; @@ -221,7 +262,11 @@ $.fn.inventreeTable = function(options) { // Extract query params var filters = options.queryParams || options.filters || {}; + // Store the total set of query params + options.query_params = filters; + options.queryParams = function(params) { + // Update the query parameters callback with the *new* filters return convertQueryParameters(params, filters); }; From 51de436d425da4afd142d9c7726704b083cbbaec Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 3 Mar 2022 17:21:04 +1100 Subject: [PATCH 03/12] User can select export format --- InvenTree/templates/js/translated/tables.js | 44 +++++++++++++++------ 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/InvenTree/templates/js/translated/tables.js b/InvenTree/templates/js/translated/tables.js index b295c3f89a..9fb5b668d1 100644 --- a/InvenTree/templates/js/translated/tables.js +++ b/InvenTree/templates/js/translated/tables.js @@ -31,33 +31,53 @@ function reloadtable(table) { * - The table has been loaded using the inventreeTable() function, not bootstrapTable() * (Refer to the "reloadTableFilters" function to see why!) */ -function downloadTableData(table) { +function downloadTableData(table, opts={}) { // Extract table configuration options - var options = table.bootstrapTable('getOptions'); + var table_options = table.bootstrapTable('getOptions'); - var url = options.url; + var url = table_options.url; if (!url) { console.log("Error: downloadTableData could not find 'url' parameter"); } - var query_params = options.query_params || {}; + var query_params = table_options.query_params || {}; url += '?'; - for (const [key, value] of Object.entries(query_params)) { - url += `${key}=${value}&`; - } + constructFormBody({}, { + title: opts.title || '{% trans "Export Table Data" %}', + fields: { + format: { + label: '{% trans "Format" %}', + help_text: '{% trans "Select File Format" %}', + required: true, + type: 'choice', + value: 'csv', + choices: exportFormatOptions(), + } + }, + onSubmit: function(fields, form_options) { + var format = getFormFieldValue('format', fields['format'], form_options); + + // Hide the modal + $(form_options.modal).modal('hide'); - var format = 'csv'; - - url += `export=${format}`; - - location.href = url; + for (const [key, value] of Object.entries(query_params)) { + url += `${key}=${value}&`; + } + + url += `export=${format}`; + + location.href = url; + } + }); } + + /** * Render a URL for display * @param {String} text From 8bf84ec217d6f9f8f124a74ad2f165843ca1a152 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 3 Mar 2022 17:25:31 +1100 Subject: [PATCH 04/12] Remove calls to "exportStock" --- .../company/templates/company/detail.html | 4 +- .../templates/company/supplier_part.html | 6 +-- InvenTree/part/templates/part/detail.html | 4 +- InvenTree/templates/js/translated/stock.js | 44 ------------------- 4 files changed, 3 insertions(+), 55 deletions(-) diff --git a/InvenTree/company/templates/company/detail.html b/InvenTree/company/templates/company/detail.html index e63f217f4b..190efd6cd4 100644 --- a/InvenTree/company/templates/company/detail.html +++ b/InvenTree/company/templates/company/detail.html @@ -283,9 +283,7 @@ }); $("#stock-export").click(function() { - exportStock({ - supplier: {{ company.id }} - }); + downloadTableData($("#stock-table")); }); {% if company.is_manufacturer %} diff --git a/InvenTree/company/templates/company/supplier_part.html b/InvenTree/company/templates/company/supplier_part.html index 89e8f91493..1fe1ea86b4 100644 --- a/InvenTree/company/templates/company/supplier_part.html +++ b/InvenTree/company/templates/company/supplier_part.html @@ -309,11 +309,7 @@ loadStockTable($("#stock-table"), { }); $("#stock-export").click(function() { - - exportStock({ - supplier_part: {{ part.pk }}, - }); - + downloadTableData($("#stock-table")); }); $("#item-create").click(function() { diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index a6cfda757f..172315b50e 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -834,9 +834,7 @@ $("#stock-export").click(function() { - exportStock({ - part: {{ part.pk }} - }); + downloadTableData($("#stock-table")); }); $('#item-create').click(function () { diff --git a/InvenTree/templates/js/translated/stock.js b/InvenTree/templates/js/translated/stock.js index 1ca89368dc..6239a886ab 100644 --- a/InvenTree/templates/js/translated/stock.js +++ b/InvenTree/templates/js/translated/stock.js @@ -43,7 +43,6 @@ duplicateStockItem, editStockItem, editStockLocation, - exportStock, findStockItemBySerialNumber, installStockItem, loadInstalledInTable, @@ -506,49 +505,6 @@ function stockStatusCodes() { } -/* - * Export stock table - */ -function exportStock(params={}) { - - constructFormBody({}, { - title: '{% trans "Export Stock" %}', - fields: { - format: { - label: '{% trans "Format" %}', - help_text: '{% trans "Select file format" %}', - required: true, - type: 'choice', - value: 'csv', - choices: exportFormatOptions(), - }, - sublocations: { - label: '{% trans "Include Sublocations" %}', - help_text: '{% trans "Include stock items in sublocations" %}', - type: 'boolean', - value: 'true', - } - }, - onSubmit: function(fields, form_options) { - - var format = getFormFieldValue('format', fields['format'], form_options); - var cascade = getFormFieldValue('sublocations', fields['sublocations'], form_options); - - // Hide the modal - $(form_options.modal).modal('hide'); - - var url = `{% url "stock-export" %}?format=${format}&cascade=${cascade}`; - - for (var key in params) { - url += `&${key}=${params[key]}`; - } - - location.href = url; - } - }); -} - - /** * Assign multiple stock items to a customer */ From 10cc72910d42b118553d5c840226ea0959c6f44c Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 3 Mar 2022 17:25:41 +1100 Subject: [PATCH 05/12] Remove StockExport view (no longer required!) --- InvenTree/stock/urls.py | 2 - InvenTree/stock/views.py | 89 ---------------------------------------- 2 files changed, 91 deletions(-) diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py index b2536e0b97..bb4a56e2dc 100644 --- a/InvenTree/stock/urls.py +++ b/InvenTree/stock/urls.py @@ -47,8 +47,6 @@ stock_urls = [ url(r'^track/', include(stock_tracking_urls)), - url(r'^export/?', views.StockExport.as_view(), name='stock-export'), - # Individual stock items url(r'^item/(?P\d+)/', include(stock_item_detail_urls)), diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index d1fde25b0a..f4cc6bc05d 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -380,95 +380,6 @@ class StockItemDeleteTestData(AjaxUpdateView): return self.renderJsonResponse(request, form, data) -class StockExport(AjaxView): - """ Export stock data from a particular location. - Returns a file containing stock information for that location. - """ - - model = StockItem - role_required = 'stock.view' - - def get(self, request, *args, **kwargs): - - export_format = request.GET.get('format', 'csv').lower() - - # Check if a particular location was specified - loc_id = request.GET.get('location', None) - location = None - - if loc_id: - try: - location = StockLocation.objects.get(pk=loc_id) - except (ValueError, StockLocation.DoesNotExist): - pass - - # Check if a particular supplier was specified - sup_id = request.GET.get('supplier', None) - supplier = None - - if sup_id: - try: - supplier = Company.objects.get(pk=sup_id) - except (ValueError, Company.DoesNotExist): - pass - - # Check if a particular supplier_part was specified - sup_part_id = request.GET.get('supplier_part', None) - supplier_part = None - - if sup_part_id: - try: - supplier_part = SupplierPart.objects.get(pk=sup_part_id) - except (ValueError, SupplierPart.DoesNotExist): - pass - - # Check if a particular part was specified - part_id = request.GET.get('part', None) - part = None - - if part_id: - try: - part = Part.objects.get(pk=part_id) - except (ValueError, Part.DoesNotExist): - pass - - if export_format not in GetExportFormats(): - export_format = 'csv' - - filename = 'InvenTree_Stocktake_{date}.{fmt}'.format( - date=datetime.now().strftime("%d-%b-%Y"), - fmt=export_format - ) - - if location: - # Check if locations should be cascading - cascade = str2bool(request.GET.get('cascade', True)) - stock_items = location.get_stock_items(cascade) - else: - stock_items = StockItem.objects.all() - - if part: - stock_items = stock_items.filter(part=part) - - if supplier: - stock_items = stock_items.filter(supplier_part__supplier=supplier) - - if supplier_part: - stock_items = stock_items.filter(supplier_part=supplier_part) - - # Filter out stock items that are not 'in stock' - stock_items = stock_items.filter(StockItem.IN_STOCK_FILTER) - - # Pre-fetch related fields to reduce DB queries - stock_items = stock_items.prefetch_related('part', 'supplier_part__supplier', 'location', 'purchase_order', 'build') - - dataset = StockItemResource().export(queryset=stock_items) - - filedata = dataset.export(export_format) - - return DownloadFile(filedata, filename) - - class StockItemQRCode(QRCodeView): """ View for displaying a QR code for a StockItem object """ From 73a32f66c8bb920870244b1013cbe7f30021a41d Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 3 Mar 2022 17:31:25 +1100 Subject: [PATCH 06/12] Adds ability to export stock "assigned" to a particular customer --- InvenTree/company/templates/company/detail.html | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/InvenTree/company/templates/company/detail.html b/InvenTree/company/templates/company/detail.html index 190efd6cd4..3c9aa92c9a 100644 --- a/InvenTree/company/templates/company/detail.html +++ b/InvenTree/company/templates/company/detail.html @@ -169,7 +169,12 @@
- {% include "filter_list.html" with id="customerstock" %} +
+ + {% include "filter_list.html" with id="customerstock" %} +
@@ -228,6 +233,10 @@ filterTarget: '#filter-list-customerstock', }); + $('#assigned-stock-export').click(function() { + downloadTableData($('#assigned-stock-table')); + }); + {% if company.is_customer %} loadSalesOrderTable("#sales-order-table", { url: "{% url 'api-so-list' %}", From 4f74a27e1af8ac95552028e919d704ae8144e9e3 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 3 Mar 2022 17:42:31 +1100 Subject: [PATCH 07/12] Exporting data from a Part table now uses the API too - Makes use of the existing table filters - Exported data matches exactly what you see in the table! --- InvenTree/part/api.py | 19 +++++++ InvenTree/part/templates/part/category.html | 11 ++-- InvenTree/part/test_views.py | 8 --- InvenTree/part/urls.py | 3 - InvenTree/part/views.py | 63 --------------------- 5 files changed, 23 insertions(+), 81 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 954060c456..4136014a84 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -26,6 +26,8 @@ from djmoney.contrib.exchange.exceptions import MissingRate from decimal import Decimal, InvalidOperation +from part.admin import PartResource + from .models import Part, PartCategory, PartRelated from .models import BomItem, BomItemSubstitute from .models import PartParameter, PartParameterTemplate @@ -43,6 +45,7 @@ from build.models import Build from . import serializers as part_serializers from InvenTree.helpers import str2bool, isNull, increment +from InvenTree.helpers import DownloadFile from InvenTree.api import AttachmentMixin from InvenTree.status_codes import BuildStatus @@ -726,6 +729,22 @@ class PartList(generics.ListCreateAPIView): queryset = self.filter_queryset(self.get_queryset()) + # Check if we wish to export the queried data to a file. + # If so, skip pagination! + export_format = request.query_params.get('export', None) + + if export_format: + export_format = str(export_format).strip().lower() + + if export_format in ['csv', 'tsv', 'xls', 'xlsx']: + dataset = PartResource().export(queryset=queryset) + + filedata = dataset.export(export_format) + + filename = f"InvenTree_Parts.{export_format}" + + return DownloadFile(filedata, filename) + page = self.paginate_queryset(queryset) if page is not None: diff --git a/InvenTree/part/templates/part/category.html b/InvenTree/part/templates/part/category.html index e02c77509d..8f30b9b46a 100644 --- a/InvenTree/part/templates/part/category.html +++ b/InvenTree/part/templates/part/category.html @@ -153,9 +153,6 @@

{% trans "Parts" %}

{% include "spacer.html" %}
- {% if roles.part.add %} @@ -291,10 +291,7 @@ }); $("#part-export").click(function() { - - var url = "{% url 'part-export' %}?category={{ category.id }}"; - - location.href = url; + downloadTableData($('#part-table')); }); {% if roles.part.add %} diff --git a/InvenTree/part/test_views.py b/InvenTree/part/test_views.py index 5b6c460e1b..2171a09b17 100644 --- a/InvenTree/part/test_views.py +++ b/InvenTree/part/test_views.py @@ -58,14 +58,6 @@ class PartListTest(PartViewTestCase): self.assertIn('parts', keys) self.assertIn('user', keys) - def test_export(self): - """ Export part data to CSV """ - - response = self.client.get(reverse('part-export'), {'parts': '1,2,3,4,5,6,7,8,9,10'}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - - self.assertEqual(response.status_code, 200) - self.assertIn('streaming_content', dir(response)) - class PartDetailTest(PartViewTestCase): diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index ba843f7d4b..55a3dc52eb 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -80,9 +80,6 @@ part_urls = [ # Download a BOM upload template url(r'^bom_template/?', views.BomUploadTemplate.as_view(), name='bom-upload-template'), - # Export data for multiple parts - url(r'^export/', views.PartExport.as_view(), name='part-export'), - # Individual part using pk url(r'^(?P\d+)/', include(part_detail_urls)), diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index e0992364dd..bba84e2d24 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -709,69 +709,6 @@ class BomUpload(InvenTreeRoleMixin, DetailView): template_name = 'part/upload_bom.html' -class PartExport(AjaxView): - """ Export a CSV file containing information on multiple parts """ - - role_required = 'part.view' - - def get_parts(self, request): - """ Extract part list from the POST parameters. - Parts can be supplied as: - - - Part category - - List of part PK values - """ - - # Filter by part category - cat_id = request.GET.get('category', None) - - part_list = None - - if cat_id is not None: - try: - category = PartCategory.objects.get(pk=cat_id) - part_list = category.get_parts() - except (ValueError, PartCategory.DoesNotExist): - pass - - # Backup - All parts - if part_list is None: - part_list = Part.objects.all() - - # Also optionally filter by explicit list of part IDs - part_ids = request.GET.get('parts', '') - parts = [] - - for pk in part_ids.split(','): - try: - parts.append(int(pk)) - except ValueError: - pass - - if len(parts) > 0: - part_list = part_list.filter(pk__in=parts) - - # Prefetch related fields to reduce DB hits - part_list = part_list.prefetch_related( - 'category', - 'used_in', - 'builds', - 'supplier_parts__purchase_order_line_items', - 'stock_items__allocations', - ) - - return part_list - - def get(self, request, *args, **kwargs): - - parts = self.get_parts(request) - - dataset = PartResource().export(queryset=parts) - - csv = dataset.export('csv') - return DownloadFile(csv, 'InvenTree_Parts.csv') - - class BomUploadTemplate(AjaxView): """ Provide a BOM upload template file for download. From 0ec0f55e17f4958dcb7e7b36a1d87f550c138a5f Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 3 Mar 2022 17:44:12 +1100 Subject: [PATCH 08/12] Style fixes --- InvenTree/part/views.py | 4 +--- InvenTree/stock/api.py | 2 +- InvenTree/stock/views.py | 6 ++---- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index bba84e2d24..4b1e0eca33 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -49,13 +49,11 @@ from . import settings as part_settings from .bom import MakeBomTemplate, ExportBom, IsValidBOMFormat from order.models import PurchaseOrderLineItem -from .admin import PartResource - from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView from InvenTree.views import QRCodeView from InvenTree.views import InvenTreeRoleMixin -from InvenTree.helpers import DownloadFile, str2bool +from InvenTree.helpers import str2bool class PartIndex(InvenTreeRoleMixin, ListView): diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index f60f1c535f..34563b38d7 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -626,7 +626,7 @@ class StockList(generics.ListCreateAPIView): dataset = StockItemResource().export(queryset=queryset) filedata = dataset.export(export_format) - + filename = 'InvenTree_Stocktake_{date}.{fmt}'.format( date=datetime.now().strftime("%d-%b-%Y"), fmt=export_format diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index f4cc6bc05d..95cb498739 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -25,13 +25,13 @@ from InvenTree.views import QRCodeView from InvenTree.views import InvenTreeRoleMixin from InvenTree.forms import ConfirmForm -from InvenTree.helpers import str2bool, DownloadFile, GetExportFormats +from InvenTree.helpers import str2bool from InvenTree.helpers import extract_serial_numbers from decimal import Decimal, InvalidOperation from datetime import datetime, timedelta -from company.models import Company, SupplierPart +from company.models import SupplierPart from part.models import Part from .models import StockItem, StockLocation, StockItemTracking @@ -39,8 +39,6 @@ import common.settings from common.models import InvenTreeSetting from users.models import Owner -from .admin import StockItemResource - from . import forms as StockForms From 85e9c4d3ca514891c8e85b4706c40eac5f9fa4ac Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 3 Mar 2022 17:47:23 +1100 Subject: [PATCH 09/12] JS linting --- InvenTree/templates/js/translated/tables.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/templates/js/translated/tables.js b/InvenTree/templates/js/translated/tables.js index 9fb5b668d1..8a6674299c 100644 --- a/InvenTree/templates/js/translated/tables.js +++ b/InvenTree/templates/js/translated/tables.js @@ -39,7 +39,7 @@ function downloadTableData(table, opts={}) { var url = table_options.url; if (!url) { - console.log("Error: downloadTableData could not find 'url' parameter"); + console.log('Error: downloadTableData could not find "url" parameter.'); } var query_params = table_options.query_params || {}; From ba406a4da9289756849c7f40a3098757de1f77ad Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 3 Mar 2022 21:16:27 +1100 Subject: [PATCH 10/12] Refactorin' - Add the "download" button into the "filters" list - Cuts down on boilerplate code --- InvenTree/company/templates/company/detail.html | 11 ----------- .../company/templates/company/supplier_part.html | 4 ---- InvenTree/part/templates/part/category.html | 7 ------- InvenTree/part/templates/part/detail.html | 7 +------ InvenTree/stock/templates/stock/location.html | 4 ---- InvenTree/templates/js/translated/filters.js | 16 ++++++++++++++-- InvenTree/templates/js/translated/part.js | 2 +- InvenTree/templates/js/translated/stock.js | 2 +- InvenTree/templates/stock_table.html | 3 --- 9 files changed, 17 insertions(+), 39 deletions(-) diff --git a/InvenTree/company/templates/company/detail.html b/InvenTree/company/templates/company/detail.html index 3c9aa92c9a..0717d02d4d 100644 --- a/InvenTree/company/templates/company/detail.html +++ b/InvenTree/company/templates/company/detail.html @@ -170,9 +170,6 @@
- {% include "filter_list.html" with id="customerstock" %}
@@ -233,10 +230,6 @@ filterTarget: '#filter-list-customerstock', }); - $('#assigned-stock-export').click(function() { - downloadTableData($('#assigned-stock-table')); - }); - {% if company.is_customer %} loadSalesOrderTable("#sales-order-table", { url: "{% url 'api-so-list' %}", @@ -291,10 +284,6 @@ filterKey: "companystock", }); - $("#stock-export").click(function() { - downloadTableData($("#stock-table")); - }); - {% if company.is_manufacturer %} function reloadManufacturerPartTable() { diff --git a/InvenTree/company/templates/company/supplier_part.html b/InvenTree/company/templates/company/supplier_part.html index 1fe1ea86b4..44e6756845 100644 --- a/InvenTree/company/templates/company/supplier_part.html +++ b/InvenTree/company/templates/company/supplier_part.html @@ -308,10 +308,6 @@ loadStockTable($("#stock-table"), { url: "{% url 'api-stock-list' %}", }); -$("#stock-export").click(function() { - downloadTableData($("#stock-table")); -}); - $("#item-create").click(function() { createNewStockItem({ data: { diff --git a/InvenTree/part/templates/part/category.html b/InvenTree/part/templates/part/category.html index 8f30b9b46a..acbd0b16f1 100644 --- a/InvenTree/part/templates/part/category.html +++ b/InvenTree/part/templates/part/category.html @@ -164,9 +164,6 @@
- @@ -290,10 +287,6 @@ }); }); - $("#part-export").click(function() { - downloadTableData($('#part-table')); - }); - {% if roles.part.add %} $("#part-create").click(function() { diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index 172315b50e..9007d91839 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -831,12 +831,7 @@ ], url: "{% url 'api-stock-list' %}", }); - - $("#stock-export").click(function() { - - downloadTableData($("#stock-table")); - }); - + $('#item-create').click(function () { createNewStockItem({ data: { diff --git a/InvenTree/stock/templates/stock/location.html b/InvenTree/stock/templates/stock/location.html index 575a798fb2..1da0030bc6 100644 --- a/InvenTree/stock/templates/stock/location.html +++ b/InvenTree/stock/templates/stock/location.html @@ -239,10 +239,6 @@ }); {% endif %} - $("#stock-export").click(function() { - downloadTableData($('#stock-table')); - }); - $('#location-create').click(function () { createStockLocation({ diff --git a/InvenTree/templates/js/translated/filters.js b/InvenTree/templates/js/translated/filters.js index 78ed30eefa..1a8c0267ee 100644 --- a/InvenTree/templates/js/translated/filters.js +++ b/InvenTree/templates/js/translated/filters.js @@ -256,7 +256,7 @@ function generateFilterInput(tableKey, filterKey) { * @param {*} table - bootstrapTable element to update * @param {*} target - name of target element on page */ -function setupFilterList(tableKey, table, target) { +function setupFilterList(tableKey, table, target, options={}) { var addClicked = false; @@ -283,6 +283,11 @@ function setupFilterList(tableKey, table, target) { var buttons = ''; + // Add download button + if (options.download) { + buttons += ``; + } + buttons += ``; // If there are filters defined for this table, add more buttons @@ -295,7 +300,7 @@ function setupFilterList(tableKey, table, target) { } element.html(` -
+
${buttons}
`); @@ -322,6 +327,13 @@ function setupFilterList(tableKey, table, target) { $(table).bootstrapTable('refresh'); }); + // Add a callback for downloading table data + if (options.download) { + element.find(`#download-${tableKey}`).click(function() { + downloadTableData($(table)); + }); + } + // Add a callback for adding a new filter element.find(`#${add}`).click(function clicked() { diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js index d42755a0f0..a0887f9473 100644 --- a/InvenTree/templates/js/translated/part.js +++ b/InvenTree/templates/js/translated/part.js @@ -1218,7 +1218,7 @@ function loadPartTable(table, url, options={}) { filters[key] = params[key]; } - setupFilterList('parts', $(table), options.filterTarget || null); + setupFilterList('parts', $(table), options.filterTarget, {download: true}); var columns = [ { diff --git a/InvenTree/templates/js/translated/stock.js b/InvenTree/templates/js/translated/stock.js index 6239a886ab..dcca969a28 100644 --- a/InvenTree/templates/js/translated/stock.js +++ b/InvenTree/templates/js/translated/stock.js @@ -1571,7 +1571,7 @@ function loadStockTable(table, options) { original[k] = params[k]; } - setupFilterList(filterKey, table, filterTarget); + setupFilterList(filterKey, table, filterTarget, {download: true}); // Override the default values, or add new ones for (var key in params) { diff --git a/InvenTree/templates/stock_table.html b/InvenTree/templates/stock_table.html index a8a4ec6691..d609e78253 100644 --- a/InvenTree/templates/stock_table.html +++ b/InvenTree/templates/stock_table.html @@ -11,9 +11,6 @@
- {% if barcodes %}
From 0af636d2b1d2e038225fd7ed29928ffb22a9d662 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 3 Mar 2022 21:23:01 +1100 Subject: [PATCH 11/12] Pass options back through when re-creating filter list --- InvenTree/templates/js/translated/filters.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/InvenTree/templates/js/translated/filters.js b/InvenTree/templates/js/translated/filters.js index 1a8c0267ee..ceef79f66d 100644 --- a/InvenTree/templates/js/translated/filters.js +++ b/InvenTree/templates/js/translated/filters.js @@ -370,14 +370,14 @@ function setupFilterList(tableKey, table, target, options={}) { reloadTableFilters(table, filters); // Run this function again - setupFilterList(tableKey, table, target); + setupFilterList(tableKey, table, target, options); } }); } else { addClicked = false; - setupFilterList(tableKey, table, target); + setupFilterList(tableKey, table, target, options); } }); @@ -388,7 +388,7 @@ function setupFilterList(tableKey, table, target, options={}) { reloadTableFilters(table, filters); - setupFilterList(tableKey, table, target); + setupFilterList(tableKey, table, target, options); }); // Add callback for deleting each filter @@ -402,7 +402,7 @@ function setupFilterList(tableKey, table, target, options={}) { reloadTableFilters(table, filters); // Run this function again! - setupFilterList(tableKey, table, target); + setupFilterList(tableKey, table, target, options); }); } From 0ba71956cd0ac0af9f3773311f34c7407337dbcf Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 4 Mar 2022 00:02:30 +1100 Subject: [PATCH 12/12] Add unit tests --- InvenTree/part/templates/part/detail.html | 5 --- InvenTree/stock/test_api.py | 53 +++++++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index 9007d91839..462389c476 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -28,11 +28,6 @@
- {% if part.is_template %} -
- {% blocktrans with full_name=part.full_name%}Showing stock for all variants of {{full_name}}{% endblocktrans %} -
- {% endif %} {% include "stock_table.html" %}
diff --git a/InvenTree/stock/test_api.py b/InvenTree/stock/test_api.py index a9dbe9e723..81973aed31 100644 --- a/InvenTree/stock/test_api.py +++ b/InvenTree/stock/test_api.py @@ -6,9 +6,12 @@ Unit testing for the Stock API from __future__ import unicode_literals import os +import io +import tablib from datetime import datetime, timedelta +import django.http from django.urls import reverse from rest_framework import status @@ -261,6 +264,56 @@ class StockItemListTest(StockAPITestCase): self.assertEqual(len(response['results']), n) + def export_data(self, filters=None): + + if not filters: + filters = {} + + filters['export'] = 'csv' + + response = self.client.get(self.list_url, data=filters) + + self.assertEqual(response.status_code, 200) + + self.assertTrue(isinstance(response, django.http.response.StreamingHttpResponse)) + + file_object = io.StringIO(response.getvalue().decode('utf-8')) + + dataset = tablib.Dataset().load(file_object, 'csv', headers=True) + + return dataset + + def test_export(self): + """ + Test exporting of Stock data via the API + """ + + dataset = self.export_data({}) + + self.assertEqual(len(dataset), 20) + + # Expected headers + headers = [ + 'part', + 'customer', + 'location', + 'parent', + 'quantity', + 'status', + ] + + for h in headers: + self.assertIn(h, dataset.headers) + + # Now, add a filter to the results + dataset = self.export_data({'location': 1}) + + self.assertEqual(len(dataset), 2) + + dataset = self.export_data({'part': 25}) + + self.assertEqual(len(dataset), 8) + class StockItemTest(StockAPITestCase): """