From 2c969ef1c60dfc6854e3e0e92e3f5dd40904a9f4 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 8 Sep 2019 10:57:19 +1000 Subject: [PATCH 01/12] View for exporting stocktake / stock list (cherry picked from commit bdad2d6178a14322ef225d08b13db86b6d7d0909) --- InvenTree/stock/urls.py | 2 ++ InvenTree/stock/views.py | 38 +++++++++++++++++++++++++++++++++++++- Makefile | 2 +- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py index 76fbaae669..19dbecd88a 100644 --- a/InvenTree/stock/urls.py +++ b/InvenTree/stock/urls.py @@ -51,6 +51,8 @@ stock_urls = [ url(r'^adjust/?', views.StockAdjust.as_view(), name='stock-adjust'), + 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 1431cb3693..46d33e534e 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -18,7 +18,7 @@ from InvenTree.views import AjaxView from InvenTree.views import AjaxUpdateView, AjaxDeleteView, AjaxCreateView from InvenTree.views import QRCodeView -from InvenTree.helpers import str2bool +from InvenTree.helpers import str2bool, DownloadFile from InvenTree.helpers import ExtractSerialNumbers from datetime import datetime @@ -119,6 +119,42 @@ class StockLocationQRCode(QRCodeView): return None +class StockExport(AjaxView): + """ Export stock data from a particular location. + Returns a file containing stock information for that location. + """ + + model = StockItem + + def get(self, request, *args, **kwargs): + + location = None + loc_id = request.GET.get('location', None) + path = 'All-Locations' + + if loc_id: + try: + location = StockLocation.objects.get(pk=loc_id) + path = location.pathstring.replace('/', ':') + except (ValueError, StockLocation.DoesNotExist): + pass + + export_format = request.GET.get('format', 'csv').lower() + + if export_format not in ['csv', 'xls', 'xslx']: + export_format = 'csv' + + filename = 'InvenTree_Stocktake_{loc}_{date}.{fmt}'.format( + loc=path, + date=datetime.now().strftime("%d-%b-%Y"), + fmt=export_format + ) + + filedata = "" + + return DownloadFile(filedata, filename) + + class StockItemQRCode(QRCodeView): """ View for displaying a QR code for a StockItem object """ diff --git a/Makefile b/Makefile index 38446635da..f6b054dab0 100644 --- a/Makefile +++ b/Makefile @@ -50,7 +50,7 @@ test: # Run code coverage coverage: python3 InvenTree/manage.py check - coverage run InvenTree/manage.py test build common company order part stock InvenTree + coverage run InvenTree/manage.py test build common company order part stock InvenTree coverage html # Install packages required to generate code docs From f4e71d6055594b5343ef6ce356bd0fffd5aa11b1 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 8 Sep 2019 10:57:27 +1000 Subject: [PATCH 02/12] Add a buttony-boy (cherry picked from commit 69ac5d870a2f1bc9589cd9b23212d3b51cf92c80) --- InvenTree/stock/templates/stock/location.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/InvenTree/stock/templates/stock/location.html b/InvenTree/stock/templates/stock/location.html index 30adba9336..604b91e758 100644 --- a/InvenTree/stock/templates/stock/location.html +++ b/InvenTree/stock/templates/stock/location.html @@ -16,6 +16,9 @@ + {% if location %} {% include "qr_button.html" %} + + {% if order.status == OrderStatus.PENDING and order.lines.count > 0 %} + + {% elif order.status == OrderStatus.PLACED %} + + {% endif %} + + +

@@ -65,13 +86,6 @@ InvenTree | {{ order }} {% if order.status == OrderStatus.PENDING %} {% endif %} - - {% if order.status == OrderStatus.PENDING and order.lines.count > 0 %} - - {% elif order.status == OrderStatus.PLACED %} - - {% endif %} -

Order Items

diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index ab482b38f8..c831cd76a9 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -176,7 +176,6 @@ class StockExport(AjaxView): except (ValueError, StockLocation.DoesNotExist): location = None - if export_format not in GetExportFormats(): export_format = 'csv' @@ -186,7 +185,6 @@ class StockExport(AjaxView): fmt=export_format ) - if location: stock_items = location.get_stock_items(cascade) else: From e81a4ffacd3c264e0b62417730c6355364450baf Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 8 Sep 2019 23:36:25 +1000 Subject: [PATCH 08/12] Add docs for common modules --- docs/modules.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/modules.rst b/docs/modules.rst index 06078ad6e2..6509965972 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -9,6 +9,7 @@ InvenTree Modules docs/InvenTree/index docs/build/index + docs/common/index docs/company/index docs/part/index docs/order/index @@ -18,6 +19,7 @@ The InvenTree Django ecosystem provides the following 'apps' for core functional * `InvenTree `_ - High level management functions * `Build `_ - Part build projects +* `Common `_ - Common modules used by various apps * `Company `_ - Company management (suppliers / customers) * `Part `_ - Part management * `Order `_ - Order management From 3d5542181aa5c89619b1b659ca0230700b4967f6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 8 Sep 2019 23:40:51 +1000 Subject: [PATCH 09/12] Move "Export" button onto stock table --- InvenTree/stock/templates/stock/location.html | 5 +---- InvenTree/templates/stock_table.html | 1 + 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/InvenTree/stock/templates/stock/location.html b/InvenTree/stock/templates/stock/location.html index fa0db9efce..9878aa56f6 100644 --- a/InvenTree/stock/templates/stock/location.html +++ b/InvenTree/stock/templates/stock/location.html @@ -16,9 +16,6 @@ - {% if location %} {% include "qr_button.html" %} {% if not part or part.is_template == False %} {% endif %} From 231a669fe53db16315abb2ea624e6f18a4dff831 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 8 Sep 2019 23:53:09 +1000 Subject: [PATCH 10/12] Export stock based on supplier --- .../templates/company/detail_stock.html | 14 +++++ InvenTree/stock/templates/stock/location.html | 28 +++++----- InvenTree/stock/views.py | 52 +++++++++++++++---- 3 files changed, 69 insertions(+), 25 deletions(-) diff --git a/InvenTree/company/templates/company/detail_stock.html b/InvenTree/company/templates/company/detail_stock.html index 95c7d1f6d1..43966810b8 100644 --- a/InvenTree/company/templates/company/detail_stock.html +++ b/InvenTree/company/templates/company/detail_stock.html @@ -27,4 +27,18 @@ ] }); + $("#stock-export").click(function() { + launchModalForm("{% url 'stock-export-options' %}", { + submit_text: "Export", + success: function(response) { + var url = "{% url 'stock-export' %}"; + + url += "?format=" + response.format; + url += "&supplier={{ company.id }}"; + + location.href = url; + }, + }); + }); + {% endblock %} \ No newline at end of file diff --git a/InvenTree/stock/templates/stock/location.html b/InvenTree/stock/templates/stock/location.html index 9878aa56f6..6a0d05cfcc 100644 --- a/InvenTree/stock/templates/stock/location.html +++ b/InvenTree/stock/templates/stock/location.html @@ -68,23 +68,21 @@ }); $("#stock-export").click(function() { - launchModalForm("{% url 'stock-export-options' %}", - { - submit_text: "Export", - success: function(response) { - var url = "{% url 'stock-export' %}"; - - url += "?format=" + response.format; - url += "&cascade=" + response.cascade; - - {% if location %} - url += "&location={{ location.id }}"; - {% endif %} + launchModalForm("{% url 'stock-export-options' %}", { + submit_text: "Export", + success: function(response) { + var url = "{% url 'stock-export' %}"; + + url += "?format=" + response.format; + url += "&cascade=" + response.cascade; + + {% if location %} + url += "&location={{ location.id }}"; + {% endif %} - location.href = url; - } + location.href = url; } - ); + }); }); $('#location-create').click(function () { diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index c831cd76a9..6d924361a8 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -24,6 +24,7 @@ from datetime import datetime import tablib +from company.models import Company from part.models import Part from .models import StockItem, StockLocation, StockItemTracking @@ -164,33 +165,59 @@ class StockExport(AjaxView): def get(self, request, *args, **kwargs): export_format = request.GET.get('format', 'csv').lower() - cascade = str2bool(request.GET.get('cascade', True)) - location = None + + # Check if a particular location was specified loc_id = request.GET.get('location', None) - path = 'All-Locations' - + location = None + if loc_id: try: location = StockLocation.objects.get(pk=loc_id) - path = location.pathstring.replace('/', ':') except (ValueError, StockLocation.DoesNotExist): - location = None + 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 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_{loc}_{date}.{fmt}'.format( - loc=path, + 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: cascade = True 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) + # Filter out stock items that are not 'in stock' stock_items = stock_items.filter(customer=None) stock_items = stock_items.filter(belongs_to=None) @@ -235,8 +262,13 @@ class StockExport(AjaxView): line.append('') line.append('') - line.append(item.location.pk) - line.append(item.location.name) + if item.location: + line.append(item.location.pk) + line.append(item.location.name) + else: + line.append('') + line.append('') + line.append(item.quantity) line.append(item.batch) line.append(item.serial) From fff42e7dbbec10b98fe21bcd5f3d28ea8f3342b1 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 8 Sep 2019 23:58:40 +1000 Subject: [PATCH 11/12] Export stock based on part --- .../InvenTree/static/script/inventree/stock.js | 4 +++- InvenTree/part/templates/part/stock.html | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/static/script/inventree/stock.js b/InvenTree/InvenTree/static/script/inventree/stock.js index e4fa188598..ae07be1dd3 100644 --- a/InvenTree/InvenTree/static/script/inventree/stock.js +++ b/InvenTree/InvenTree/static/script/inventree/stock.js @@ -76,12 +76,14 @@ function loadStockTable(table, options) { } else if (field == 'quantity') { var stock = 0; + var items = 0; data.forEach(function(item) { stock += item.quantity; + items += 1; }); - return stock; + return stock + " (" + items + " items)"; } else if (field == 'batch') { var batches = []; diff --git a/InvenTree/part/templates/part/stock.html b/InvenTree/part/templates/part/stock.html index a41d5fd32a..6050137b69 100644 --- a/InvenTree/part/templates/part/stock.html +++ b/InvenTree/part/templates/part/stock.html @@ -47,6 +47,21 @@ url: "{% url 'api-stock-list' %}", }); + $("#stock-export").click(function() { + launchModalForm("{% url 'stock-export-options' %}", { + submit_text: "Export", + success: function(response) { + var url = "{% url 'stock-export' %}"; + + url += "?format=" + response.format; + url += "&cascade=" + response.cascade; + url += "&part={{ part.id }}"; + + location.href = url; + }, + }); + }); + $('#item-create').click(function () { launchModalForm("{% url 'stock-item-create' %}", { reload: true, From 11c946be4d9eae6eb6fedda95ffdc058f5a3fd9c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 9 Sep 2019 00:02:08 +1000 Subject: [PATCH 12/12] Export human-readable status code --- InvenTree/InvenTree/status_codes.py | 2 +- InvenTree/stock/views.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/status_codes.py b/InvenTree/InvenTree/status_codes.py index b7b50a58f7..f772943ef0 100644 --- a/InvenTree/InvenTree/status_codes.py +++ b/InvenTree/InvenTree/status_codes.py @@ -10,7 +10,7 @@ class StatusCode: @classmethod def label(cls, value): """ Return the status code label associated with the provided value """ - return cls.options.get(value, '') + return cls.options.get(value, value) class OrderStatus(StatusCode): diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 6d924361a8..29b5c5d6bb 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -18,6 +18,7 @@ from InvenTree.views import AjaxView from InvenTree.views import AjaxUpdateView, AjaxDeleteView, AjaxCreateView from InvenTree.views import QRCodeView +from InvenTree.status_codes import StockStatus from InvenTree.helpers import str2bool, DownloadFile, GetExportFormats from InvenTree.helpers import ExtractSerialNumbers from datetime import datetime @@ -272,7 +273,7 @@ class StockExport(AjaxView): line.append(item.quantity) line.append(item.batch) line.append(item.serial) - line.append(item.status) + line.append(StockStatus.label(item.status)) line.append(item.notes) line.append(item.review_needed) line.append(item.updated)