diff --git a/InvenTree/order/templates/order/purchase_orders.html b/InvenTree/order/templates/order/purchase_orders.html
index 78649ca6de..0e93c6d900 100644
--- a/InvenTree/order/templates/order/purchase_orders.html
+++ b/InvenTree/order/templates/order/purchase_orders.html
@@ -27,7 +27,7 @@ InvenTree | Purchase Orders
$("#po-create").click(function() {
launchModalForm("{% url 'purchase-order-create' %}",
{
- reload: true,
+ follow: true,
}
);
});
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,
diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py
index 6ad25ff279..3a34e0416d 100644
--- a/InvenTree/stock/forms.py
+++ b/InvenTree/stock/forms.py
@@ -9,6 +9,7 @@ from django import forms
from django.forms.utils import ErrorDict
from django.utils.translation import ugettext as _
+from InvenTree.helpers import GetExportFormats
from InvenTree.forms import HelperForm
from .models import StockLocation, StockItem, StockItemTracking
@@ -96,6 +97,33 @@ class SerializeStockForm(forms.ModelForm):
]
+class ExportOptionsForm(HelperForm):
+ """ Form for selecting stock export options """
+
+ file_format = forms.ChoiceField(label=_('File Format'), help_text=_('Select output file format'))
+
+ include_sublocations = forms.BooleanField(required=False, initial=True, help_text=_("Include stock items in sub locations"))
+
+ class Meta:
+ model = StockLocation
+ fields = [
+ 'file_format',
+ 'include_sublocations',
+ ]
+
+ def get_format_choices(self):
+ """ File format choices """
+
+ choices = [(x, x.upper()) for x in GetExportFormats()]
+
+ return choices
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.fields['file_format'].choices = self.get_format_choices()
+
+
class AdjustStockForm(forms.ModelForm):
""" Form for performing simple stock adjustments.
diff --git a/InvenTree/stock/templates/stock/location.html b/InvenTree/stock/templates/stock/location.html
index 30adba9336..6a0d05cfcc 100644
--- a/InvenTree/stock/templates/stock/location.html
+++ b/InvenTree/stock/templates/stock/location.html
@@ -67,6 +67,24 @@
sessionStorage.removeItem('inventree-show-part-locations');
});
+ $("#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 %}
+
+ location.href = url;
+ }
+ });
+ });
+
$('#location-create').click(function () {
launchModalForm("{% url 'stock-location-create' %}",
{
diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py
index 76fbaae669..fa849211f3 100644
--- a/InvenTree/stock/urls.py
+++ b/InvenTree/stock/urls.py
@@ -51,6 +51,9 @@ stock_urls = [
url(r'^adjust/?', views.StockAdjust.as_view(), name='stock-adjust'),
+ url(r'^export-options/?', views.StockExportOptions.as_view(), name='stock-export-options'),
+ 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..29b5c5d6bb 100644
--- a/InvenTree/stock/views.py
+++ b/InvenTree/stock/views.py
@@ -18,10 +18,14 @@ from InvenTree.views import AjaxView
from InvenTree.views import AjaxUpdateView, AjaxDeleteView, AjaxCreateView
from InvenTree.views import QRCodeView
-from InvenTree.helpers import str2bool
+from InvenTree.status_codes import StockStatus
+from InvenTree.helpers import str2bool, DownloadFile, GetExportFormats
from InvenTree.helpers import ExtractSerialNumbers
from datetime import datetime
+import tablib
+
+from company.models import Company
from part.models import Part
from .models import StockItem, StockLocation, StockItemTracking
@@ -31,6 +35,7 @@ from .forms import EditStockItemForm
from .forms import AdjustStockForm
from .forms import TrackingEntryForm
from .forms import SerializeStockForm
+from .forms import ExportOptionsForm
class StockIndex(ListView):
@@ -119,6 +124,178 @@ class StockLocationQRCode(QRCodeView):
return None
+class StockExportOptions(AjaxView):
+ """ Form for selecting StockExport options """
+
+ model = StockLocation
+ ajax_form_title = 'Stock Export Options'
+ form_class = ExportOptionsForm
+
+ def post(self, request, *args, **kwargs):
+
+ self.request = request
+
+ fmt = request.POST.get('file_format', 'csv').lower()
+ cascade = str2bool(request.POST.get('include_sublocations', False))
+
+ # Format a URL to redirect to
+ url = reverse('stock-export')
+
+ url += '?format=' + fmt
+ url += '&cascade=' + str(cascade)
+
+ data = {
+ 'form_valid': True,
+ 'format': fmt,
+ 'cascade': cascade
+ }
+
+ return self.renderJsonResponse(self.request, self.form_class(), data=data)
+
+ def get(self, request, *args, **kwargs):
+ return self.renderJsonResponse(request, self.form_class())
+
+
+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):
+
+ 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 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:
+ 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)
+
+ # Column headers
+ headers = [
+ _('Stock ID'),
+ _('Part ID'),
+ _('Part'),
+ _('Supplier Part ID'),
+ _('Supplier ID'),
+ _('Supplier'),
+ _('Location ID'),
+ _('Location'),
+ _('Quantity'),
+ _('Batch'),
+ _('Serial'),
+ _('Status'),
+ _('Notes'),
+ _('Review Needed'),
+ _('Last Updated'),
+ _('Last Stocktake'),
+ _('Purchase Order ID'),
+ _('Build ID'),
+ ]
+
+ data = tablib.Dataset(headers=headers)
+
+ for item in stock_items:
+ line = []
+
+ line.append(item.pk)
+ line.append(item.part.pk)
+ line.append(item.part.full_name)
+
+ if item.supplier_part:
+ line.append(item.supplier_part.pk)
+ line.append(item.supplier_part.supplier.pk)
+ line.append(item.supplier_part.supplier.name)
+ else:
+ line.append('')
+ line.append('')
+ line.append('')
+
+ 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)
+ line.append(StockStatus.label(item.status))
+ line.append(item.notes)
+ line.append(item.review_needed)
+ line.append(item.updated)
+ line.append(item.stocktake_date)
+
+ if item.purchase_order:
+ line.append(item.purchase_order.pk)
+ else:
+ line.append('')
+
+ if item.build:
+ line.append(item.build.pk)
+ else:
+ line.append('')
+
+ data.append(line)
+
+ filedata = data.export(export_format)
+
+ return DownloadFile(filedata, filename)
+
+
class StockItemQRCode(QRCodeView):
""" View for displaying a QR code for a StockItem object """
diff --git a/InvenTree/templates/stock_table.html b/InvenTree/templates/stock_table.html
index e53daa4718..5b707eab24 100644
--- a/InvenTree/templates/stock_table.html
+++ b/InvenTree/templates/stock_table.html
@@ -1,5 +1,6 @@
+
{% if not part or part.is_template == False %}
{% endif %}
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
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