mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-30 04:26:44 +00:00
Merge pull request #2709 from SchrodingersGat/stock-exporter
Stock export refactor
This commit is contained in:
commit
99f3d97f13
@ -169,8 +169,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class='panel-content'>
|
<div class='panel-content'>
|
||||||
<div id='assigned-stock-button-toolbar'>
|
<div id='assigned-stock-button-toolbar'>
|
||||||
|
<div class='btn-group' role='group'>
|
||||||
{% include "filter_list.html" with id="customerstock" %}
|
{% include "filter_list.html" with id="customerstock" %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<table class='table table-striped table-condensed' id='assigned-stock-table' data-toolbar='#assigned-stock-button-toolbar'></table>
|
<table class='table table-striped table-condensed' id='assigned-stock-table' data-toolbar='#assigned-stock-button-toolbar'></table>
|
||||||
|
|
||||||
@ -282,12 +284,6 @@
|
|||||||
filterKey: "companystock",
|
filterKey: "companystock",
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#stock-export").click(function() {
|
|
||||||
exportStock({
|
|
||||||
supplier: {{ company.id }}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
{% if company.is_manufacturer %}
|
{% if company.is_manufacturer %}
|
||||||
|
|
||||||
function reloadManufacturerPartTable() {
|
function reloadManufacturerPartTable() {
|
||||||
|
@ -308,14 +308,6 @@ loadStockTable($("#stock-table"), {
|
|||||||
url: "{% url 'api-stock-list' %}",
|
url: "{% url 'api-stock-list' %}",
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#stock-export").click(function() {
|
|
||||||
|
|
||||||
exportStock({
|
|
||||||
supplier_part: {{ part.pk }},
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#item-create").click(function() {
|
$("#item-create").click(function() {
|
||||||
createNewStockItem({
|
createNewStockItem({
|
||||||
data: {
|
data: {
|
||||||
|
@ -26,6 +26,8 @@ from djmoney.contrib.exchange.exceptions import MissingRate
|
|||||||
|
|
||||||
from decimal import Decimal, InvalidOperation
|
from decimal import Decimal, InvalidOperation
|
||||||
|
|
||||||
|
from part.admin import PartResource
|
||||||
|
|
||||||
from .models import Part, PartCategory, PartRelated
|
from .models import Part, PartCategory, PartRelated
|
||||||
from .models import BomItem, BomItemSubstitute
|
from .models import BomItem, BomItemSubstitute
|
||||||
from .models import PartParameter, PartParameterTemplate
|
from .models import PartParameter, PartParameterTemplate
|
||||||
@ -43,6 +45,7 @@ from build.models import Build
|
|||||||
from . import serializers as part_serializers
|
from . import serializers as part_serializers
|
||||||
|
|
||||||
from InvenTree.helpers import str2bool, isNull, increment
|
from InvenTree.helpers import str2bool, isNull, increment
|
||||||
|
from InvenTree.helpers import DownloadFile
|
||||||
from InvenTree.api import AttachmentMixin
|
from InvenTree.api import AttachmentMixin
|
||||||
|
|
||||||
from InvenTree.status_codes import BuildStatus
|
from InvenTree.status_codes import BuildStatus
|
||||||
@ -726,6 +729,22 @@ class PartList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
queryset = self.filter_queryset(self.get_queryset())
|
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)
|
page = self.paginate_queryset(queryset)
|
||||||
|
|
||||||
if page is not None:
|
if page is not None:
|
||||||
|
@ -153,9 +153,6 @@
|
|||||||
<h4>{% trans "Parts" %}</h4>
|
<h4>{% trans "Parts" %}</h4>
|
||||||
{% include "spacer.html" %}
|
{% include "spacer.html" %}
|
||||||
<div class='btn-group' role='group'>
|
<div class='btn-group' role='group'>
|
||||||
<button type='button' class='btn btn-outline-secondary' id='part-export' title='{% trans "Export Part Data" %}'>
|
|
||||||
<span class='fas fa-file-download'></span> {% trans "Export" %}
|
|
||||||
</button>
|
|
||||||
{% if roles.part.add %}
|
{% if roles.part.add %}
|
||||||
<button type='button' class='btn btn-success' id='part-create' title='{% trans "Create new part" %}'>
|
<button type='button' class='btn btn-success' id='part-create' title='{% trans "Create new part" %}'>
|
||||||
<span class='fas fa-plus-circle'></span> {% trans "New Part" %}
|
<span class='fas fa-plus-circle'></span> {% trans "New Part" %}
|
||||||
@ -290,13 +287,6 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#part-export").click(function() {
|
|
||||||
|
|
||||||
var url = "{% url 'part-export' %}?category={{ category.id }}";
|
|
||||||
|
|
||||||
location.href = url;
|
|
||||||
});
|
|
||||||
|
|
||||||
{% if roles.part.add %}
|
{% if roles.part.add %}
|
||||||
$("#part-create").click(function() {
|
$("#part-create").click(function() {
|
||||||
|
|
||||||
|
@ -28,11 +28,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class='panel-content'>
|
<div class='panel-content'>
|
||||||
{% if part.is_template %}
|
|
||||||
<div class='alert alert-info alert-block'>
|
|
||||||
{% blocktrans with full_name=part.full_name%}Showing stock for all variants of <em>{{full_name}}</em>{% endblocktrans %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% include "stock_table.html" %}
|
{% include "stock_table.html" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -830,13 +825,6 @@
|
|||||||
url: "{% url 'api-stock-list' %}",
|
url: "{% url 'api-stock-list' %}",
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#stock-export").click(function() {
|
|
||||||
|
|
||||||
exportStock({
|
|
||||||
part: {{ part.pk }}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#item-create').click(function () {
|
$('#item-create').click(function () {
|
||||||
createNewStockItem({
|
createNewStockItem({
|
||||||
data: {
|
data: {
|
||||||
|
@ -58,14 +58,6 @@ class PartListTest(PartViewTestCase):
|
|||||||
self.assertIn('parts', keys)
|
self.assertIn('parts', keys)
|
||||||
self.assertIn('user', 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):
|
class PartDetailTest(PartViewTestCase):
|
||||||
|
|
||||||
|
@ -80,9 +80,6 @@ part_urls = [
|
|||||||
# Download a BOM upload template
|
# Download a BOM upload template
|
||||||
url(r'^bom_template/?', views.BomUploadTemplate.as_view(), name='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
|
# Individual part using pk
|
||||||
url(r'^(?P<pk>\d+)/', include(part_detail_urls)),
|
url(r'^(?P<pk>\d+)/', include(part_detail_urls)),
|
||||||
|
|
||||||
|
@ -49,13 +49,11 @@ from . import settings as part_settings
|
|||||||
from .bom import MakeBomTemplate, ExportBom, IsValidBOMFormat
|
from .bom import MakeBomTemplate, ExportBom, IsValidBOMFormat
|
||||||
from order.models import PurchaseOrderLineItem
|
from order.models import PurchaseOrderLineItem
|
||||||
|
|
||||||
from .admin import PartResource
|
|
||||||
|
|
||||||
from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView
|
from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView
|
||||||
from InvenTree.views import QRCodeView
|
from InvenTree.views import QRCodeView
|
||||||
from InvenTree.views import InvenTreeRoleMixin
|
from InvenTree.views import InvenTreeRoleMixin
|
||||||
|
|
||||||
from InvenTree.helpers import DownloadFile, str2bool
|
from InvenTree.helpers import str2bool
|
||||||
|
|
||||||
|
|
||||||
class PartIndex(InvenTreeRoleMixin, ListView):
|
class PartIndex(InvenTreeRoleMixin, ListView):
|
||||||
@ -709,69 +707,6 @@ class BomUpload(InvenTreeRoleMixin, DetailView):
|
|||||||
template_name = 'part/upload_bom.html'
|
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):
|
class BomUploadTemplate(AjaxView):
|
||||||
"""
|
"""
|
||||||
Provide a BOM upload template file for download.
|
Provide a BOM upload template file for download.
|
||||||
|
@ -30,6 +30,7 @@ from company.models import Company, SupplierPart
|
|||||||
from company.serializers import CompanySerializer, SupplierPartSerializer
|
from company.serializers import CompanySerializer, SupplierPartSerializer
|
||||||
|
|
||||||
from InvenTree.helpers import str2bool, isNull, extract_serial_numbers
|
from InvenTree.helpers import str2bool, isNull, extract_serial_numbers
|
||||||
|
from InvenTree.helpers import DownloadFile
|
||||||
from InvenTree.api import AttachmentMixin
|
from InvenTree.api import AttachmentMixin
|
||||||
from InvenTree.filters import InvenTreeOrderingFilter
|
from InvenTree.filters import InvenTreeOrderingFilter
|
||||||
|
|
||||||
@ -40,6 +41,7 @@ from order.serializers import POSerializer
|
|||||||
from part.models import BomItem, Part, PartCategory
|
from part.models import BomItem, Part, PartCategory
|
||||||
from part.serializers import PartBriefSerializer
|
from part.serializers import PartBriefSerializer
|
||||||
|
|
||||||
|
from stock.admin import StockItemResource
|
||||||
from stock.models import StockLocation, StockItem
|
from stock.models import StockLocation, StockItem
|
||||||
from stock.models import StockItemTracking
|
from stock.models import StockItemTracking
|
||||||
from stock.models import StockItemAttachment
|
from stock.models import StockItemAttachment
|
||||||
@ -611,6 +613,27 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
queryset = self.filter_queryset(self.get_queryset())
|
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)
|
page = self.paginate_queryset(queryset)
|
||||||
|
|
||||||
if page is not None:
|
if page is not None:
|
||||||
@ -641,7 +664,7 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
supplier_part_ids.add(sp)
|
supplier_part_ids.add(sp)
|
||||||
|
|
||||||
# Do we wish to include Part detail?
|
# 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
|
# Fetch only the required Part objects from the database
|
||||||
parts = Part.objects.filter(pk__in=part_ids).prefetch_related(
|
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)
|
stock_item['part_detail'] = part_map.get(part_id, None)
|
||||||
|
|
||||||
# Do we wish to include SupplierPart detail?
|
# 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)
|
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)
|
stock_item['supplier_part_detail'] = supplier_part_map.get(part_id, None)
|
||||||
|
|
||||||
# Do we wish to include StockLocation detail?
|
# 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
|
# Fetch only the required StockLocation objects from the database
|
||||||
locations = StockLocation.objects.filter(pk__in=location_ids).prefetch_related(
|
locations = StockLocation.objects.filter(pk__in=location_ids).prefetch_related(
|
||||||
|
@ -239,15 +239,6 @@
|
|||||||
});
|
});
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
$("#stock-export").click(function() {
|
|
||||||
|
|
||||||
exportStock({
|
|
||||||
{% if location %}
|
|
||||||
location: {{ location.pk }}
|
|
||||||
{% endif %}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#location-create').click(function () {
|
$('#location-create').click(function () {
|
||||||
|
|
||||||
createStockLocation({
|
createStockLocation({
|
||||||
|
@ -6,9 +6,12 @@ Unit testing for the Stock API
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import io
|
||||||
|
import tablib
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
import django.http
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
@ -261,6 +264,56 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
|
|
||||||
self.assertEqual(len(response['results']), n)
|
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):
|
class StockItemTest(StockAPITestCase):
|
||||||
"""
|
"""
|
||||||
|
@ -47,8 +47,6 @@ stock_urls = [
|
|||||||
|
|
||||||
url(r'^track/', include(stock_tracking_urls)),
|
url(r'^track/', include(stock_tracking_urls)),
|
||||||
|
|
||||||
url(r'^export/?', views.StockExport.as_view(), name='stock-export'),
|
|
||||||
|
|
||||||
# Individual stock items
|
# Individual stock items
|
||||||
url(r'^item/(?P<pk>\d+)/', include(stock_item_detail_urls)),
|
url(r'^item/(?P<pk>\d+)/', include(stock_item_detail_urls)),
|
||||||
|
|
||||||
|
@ -25,13 +25,13 @@ from InvenTree.views import QRCodeView
|
|||||||
from InvenTree.views import InvenTreeRoleMixin
|
from InvenTree.views import InvenTreeRoleMixin
|
||||||
from InvenTree.forms import ConfirmForm
|
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 InvenTree.helpers import extract_serial_numbers
|
||||||
|
|
||||||
from decimal import Decimal, InvalidOperation
|
from decimal import Decimal, InvalidOperation
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from company.models import Company, SupplierPart
|
from company.models import SupplierPart
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
from .models import StockItem, StockLocation, StockItemTracking
|
from .models import StockItem, StockLocation, StockItemTracking
|
||||||
|
|
||||||
@ -39,8 +39,6 @@ import common.settings
|
|||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
from users.models import Owner
|
from users.models import Owner
|
||||||
|
|
||||||
from .admin import StockItemResource
|
|
||||||
|
|
||||||
from . import forms as StockForms
|
from . import forms as StockForms
|
||||||
|
|
||||||
|
|
||||||
@ -380,95 +378,6 @@ class StockItemDeleteTestData(AjaxUpdateView):
|
|||||||
return self.renderJsonResponse(request, form, data)
|
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):
|
class StockItemQRCode(QRCodeView):
|
||||||
""" View for displaying a QR code for a StockItem object """
|
""" View for displaying a QR code for a StockItem object """
|
||||||
|
|
||||||
|
@ -256,7 +256,7 @@ function generateFilterInput(tableKey, filterKey) {
|
|||||||
* @param {*} table - bootstrapTable element to update
|
* @param {*} table - bootstrapTable element to update
|
||||||
* @param {*} target - name of target element on page
|
* @param {*} target - name of target element on page
|
||||||
*/
|
*/
|
||||||
function setupFilterList(tableKey, table, target) {
|
function setupFilterList(tableKey, table, target, options={}) {
|
||||||
|
|
||||||
var addClicked = false;
|
var addClicked = false;
|
||||||
|
|
||||||
@ -283,6 +283,11 @@ function setupFilterList(tableKey, table, target) {
|
|||||||
|
|
||||||
var buttons = '';
|
var buttons = '';
|
||||||
|
|
||||||
|
// Add download button
|
||||||
|
if (options.download) {
|
||||||
|
buttons += `<button id='download-${tableKey}' title='{% trans "Download data" %}' class='btn btn-outline-secondary filter-button'><span class='fas fa-download'></span></button>`;
|
||||||
|
}
|
||||||
|
|
||||||
buttons += `<button id='reload-${tableKey}' title='{% trans "Reload data" %}' class='btn btn-outline-secondary filter-button'><span class='fas fa-redo-alt'></span></button>`;
|
buttons += `<button id='reload-${tableKey}' title='{% trans "Reload data" %}' class='btn btn-outline-secondary filter-button'><span class='fas fa-redo-alt'></span></button>`;
|
||||||
|
|
||||||
// If there are filters defined for this table, add more buttons
|
// If there are filters defined for this table, add more buttons
|
||||||
@ -295,7 +300,7 @@ function setupFilterList(tableKey, table, target) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
element.html(`
|
element.html(`
|
||||||
<div class='btn-group' role='group'>
|
<div class='btn-group filter-group' role='group'>
|
||||||
${buttons}
|
${buttons}
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
@ -322,6 +327,13 @@ function setupFilterList(tableKey, table, target) {
|
|||||||
$(table).bootstrapTable('refresh');
|
$(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
|
// Add a callback for adding a new filter
|
||||||
element.find(`#${add}`).click(function clicked() {
|
element.find(`#${add}`).click(function clicked() {
|
||||||
|
|
||||||
@ -358,14 +370,14 @@ function setupFilterList(tableKey, table, target) {
|
|||||||
reloadTableFilters(table, filters);
|
reloadTableFilters(table, filters);
|
||||||
|
|
||||||
// Run this function again
|
// Run this function again
|
||||||
setupFilterList(tableKey, table, target);
|
setupFilterList(tableKey, table, target, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
addClicked = false;
|
addClicked = false;
|
||||||
|
|
||||||
setupFilterList(tableKey, table, target);
|
setupFilterList(tableKey, table, target, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
@ -376,7 +388,7 @@ function setupFilterList(tableKey, table, target) {
|
|||||||
|
|
||||||
reloadTableFilters(table, filters);
|
reloadTableFilters(table, filters);
|
||||||
|
|
||||||
setupFilterList(tableKey, table, target);
|
setupFilterList(tableKey, table, target, options);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add callback for deleting each filter
|
// Add callback for deleting each filter
|
||||||
@ -390,7 +402,7 @@ function setupFilterList(tableKey, table, target) {
|
|||||||
reloadTableFilters(table, filters);
|
reloadTableFilters(table, filters);
|
||||||
|
|
||||||
// Run this function again!
|
// Run this function again!
|
||||||
setupFilterList(tableKey, table, target);
|
setupFilterList(tableKey, table, target, options);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1218,7 +1218,7 @@ function loadPartTable(table, url, options={}) {
|
|||||||
filters[key] = params[key];
|
filters[key] = params[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
setupFilterList('parts', $(table), options.filterTarget || null);
|
setupFilterList('parts', $(table), options.filterTarget, {download: true});
|
||||||
|
|
||||||
var columns = [
|
var columns = [
|
||||||
{
|
{
|
||||||
|
@ -43,7 +43,6 @@
|
|||||||
duplicateStockItem,
|
duplicateStockItem,
|
||||||
editStockItem,
|
editStockItem,
|
||||||
editStockLocation,
|
editStockLocation,
|
||||||
exportStock,
|
|
||||||
findStockItemBySerialNumber,
|
findStockItemBySerialNumber,
|
||||||
installStockItem,
|
installStockItem,
|
||||||
loadInstalledInTable,
|
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
|
* Assign multiple stock items to a customer
|
||||||
*/
|
*/
|
||||||
@ -1615,7 +1571,7 @@ function loadStockTable(table, options) {
|
|||||||
original[k] = params[k];
|
original[k] = params[k];
|
||||||
}
|
}
|
||||||
|
|
||||||
setupFilterList(filterKey, table, filterTarget);
|
setupFilterList(filterKey, table, filterTarget, {download: true});
|
||||||
|
|
||||||
// Override the default values, or add new ones
|
// Override the default values, or add new ones
|
||||||
for (var key in params) {
|
for (var key in params) {
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
/* exported
|
/* exported
|
||||||
customGroupSorter,
|
customGroupSorter,
|
||||||
|
downloadTableData,
|
||||||
reloadtable,
|
reloadtable,
|
||||||
renderLink,
|
renderLink,
|
||||||
reloadTableFilters,
|
reloadTableFilters,
|
||||||
@ -21,6 +22,62 @@ 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, opts={}) {
|
||||||
|
|
||||||
|
// Extract table configuration options
|
||||||
|
var table_options = table.bootstrapTable('getOptions');
|
||||||
|
|
||||||
|
var url = table_options.url;
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
console.log('Error: downloadTableData could not find "url" parameter.');
|
||||||
|
}
|
||||||
|
|
||||||
|
var query_params = table_options.query_params || {};
|
||||||
|
|
||||||
|
url += '?';
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(query_params)) {
|
||||||
|
url += `${key}=${value}&`;
|
||||||
|
}
|
||||||
|
|
||||||
|
url += `export=${format}`;
|
||||||
|
|
||||||
|
location.href = url;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render a URL for display
|
* Render a URL for display
|
||||||
* @param {String} text
|
* @param {String} text
|
||||||
@ -114,6 +171,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) {
|
options.queryParams = function(tableParams) {
|
||||||
return convertQueryParameters(tableParams, params);
|
return convertQueryParameters(tableParams, params);
|
||||||
};
|
};
|
||||||
@ -221,7 +282,11 @@ $.fn.inventreeTable = function(options) {
|
|||||||
// Extract query params
|
// Extract query params
|
||||||
var filters = options.queryParams || options.filters || {};
|
var filters = options.queryParams || options.filters || {};
|
||||||
|
|
||||||
|
// Store the total set of query params
|
||||||
|
options.query_params = filters;
|
||||||
|
|
||||||
options.queryParams = function(params) {
|
options.queryParams = function(params) {
|
||||||
|
// Update the query parameters callback with the *new* filters
|
||||||
return convertQueryParameters(params, filters);
|
return convertQueryParameters(params, filters);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -11,9 +11,6 @@
|
|||||||
<div id='{{ prefix }}button-toolbar'>
|
<div id='{{ prefix }}button-toolbar'>
|
||||||
<div class='button-toolbar container-fluid' style='float: right;'>
|
<div class='button-toolbar container-fluid' style='float: right;'>
|
||||||
<div class='btn-group' role='group'>
|
<div class='btn-group' role='group'>
|
||||||
<button class='btn btn-outline-secondary' id='stock-export' title='{% trans "Export Stock Information" %}'>
|
|
||||||
<span class='fas fa-download'></span>
|
|
||||||
</button>
|
|
||||||
{% if barcodes %}
|
{% if barcodes %}
|
||||||
<!-- Barcode actions menu -->
|
<!-- Barcode actions menu -->
|
||||||
<div class='btn-group' role='group'>
|
<div class='btn-group' role='group'>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user