Merge pull request #2977 from SchrodingersGat/table-downloader

Adding "download" buttons for more tables
This commit is contained in:
Oliver 2022-05-12 13:27:11 +10:00 committed by GitHub
commit 56f36d4b4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 209 additions and 71 deletions

View File

@ -61,6 +61,44 @@ class NotFoundView(AjaxView):
return JsonResponse(data, status=404)
class APIDownloadMixin:
"""
Mixin for enabling a LIST endpoint to be downloaded a file.
To download the data, add the ?export=<fmt> to the query string.
The implementing class must provided a download_queryset method,
e.g.
def download_queryset(self, queryset, export_format):
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)
"""
def get(self, request, *args, **kwargs):
export_format = request.query_params.get('export', None)
if export_format and export_format in ['csv', 'tsv', 'xls', 'xlsx']:
queryset = self.filter_queryset(self.get_queryset())
return self.download_queryset(queryset, export_format)
else:
# Default to the parent class implementation
return super().get(request, *args, **kwargs)
def download_queryset(self, queryset, export_format):
raise NotImplementedError("download_queryset method not implemented!")
class AttachmentMixin:
"""
Mixin for creating attachment objects,

View File

@ -4,11 +4,16 @@ InvenTree API version information
# InvenTree API version
INVENTREE_API_VERSION = 47
INVENTREE_API_VERSION = 48
"""
Increment this API version number whenever there is a significant change to the API that any clients need to know about
v48 -> 2022-05-12 : https://github.com/inventree/InvenTree/pull/2977
- Adds "export to file" functionality for PurchaseOrder API endpoint
- Adds "export to file" functionality for SalesOrder API endpoint
- Adds "export to file" functionality for BuildOrder API endpoint
v47 -> 2022-05-10 : https://github.com/inventree/InvenTree/pull/2964
- Fixes barcode API error response when scanning a StockItem which does not exist
- Fixes barcode API error response when scanning a StockLocation which does not exist

View File

@ -2,9 +2,53 @@
from __future__ import unicode_literals
from django.contrib import admin
from import_export.admin import ImportExportModelAdmin
from .models import Build, BuildItem
from import_export.admin import ImportExportModelAdmin
from import_export.fields import Field
from import_export.resources import ModelResource
import import_export.widgets as widgets
from build.models import Build, BuildItem
import part.models
class BuildResource(ModelResource):
"""Class for managing import/export of Build data"""
# For some reason, we need to specify the fields individually for this ModelResource,
# but we don't for other ones.
# TODO: 2022-05-12 - Need to investigate why this is the case!
pk = Field(attribute='pk')
reference = Field(attribute='reference')
title = Field(attribute='title')
part = Field(attribute='part', widget=widgets.ForeignKeyWidget(part.models.Part))
part_name = Field(attribute='part__full_name', readonly=True)
overdue = Field(attribute='is_overdue', readonly=True, widget=widgets.BooleanWidget())
completed = Field(attribute='completed', readonly=True)
quantity = Field(attribute='quantity')
status = Field(attribute='status')
batch = Field(attribute='batch')
notes = Field(attribute='notes')
class Meta:
models = Build
skip_unchanged = True
report_skipped = False
clean_model_instances = True
exclude = [
'lft', 'rght', 'tree_id', 'level',
]
class BuildAdmin(ImportExportModelAdmin):

View File

@ -12,13 +12,15 @@ from rest_framework import filters, generics
from django_filters.rest_framework import DjangoFilterBackend
from django_filters import rest_framework as rest_filters
from InvenTree.api import AttachmentMixin
from InvenTree.helpers import str2bool, isNull
from InvenTree.api import AttachmentMixin, APIDownloadMixin
from InvenTree.helpers import str2bool, isNull, DownloadFile
from InvenTree.filters import InvenTreeOrderingFilter
from InvenTree.status_codes import BuildStatus
from .models import Build, BuildItem, BuildOrderAttachment
import build.admin
import build.serializers
from build.models import Build, BuildItem, BuildOrderAttachment
from users.models import Owner
@ -71,7 +73,7 @@ class BuildFilter(rest_filters.FilterSet):
return queryset
class BuildList(generics.ListCreateAPIView):
class BuildList(APIDownloadMixin, generics.ListCreateAPIView):
""" API endpoint for accessing a list of Build objects.
- GET: Return list of objects (with filters)
@ -123,6 +125,14 @@ class BuildList(generics.ListCreateAPIView):
return queryset
def download_queryset(self, queryset, export_format):
dataset = build.admin.BuildResource().export(queryset=queryset)
filedata = dataset.export(export_format)
filename = f"InvenTree_BuildOrders.{export_format}"
return DownloadFile(filedata, filename)
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)

View File

@ -5,8 +5,9 @@ from django.contrib import admin
from import_export.admin import ImportExportModelAdmin
from import_export.resources import ModelResource
from import_export.fields import Field
from import_export.resources import ModelResource
import import_export.widgets as widgets
from .models import PurchaseOrder, PurchaseOrderLineItem, PurchaseOrderExtraLine
from .models import SalesOrder, SalesOrderLineItem, SalesOrderExtraLine
@ -92,6 +93,23 @@ class SalesOrderAdmin(ImportExportModelAdmin):
autocomplete_fields = ('customer',)
class PurchaseOrderResource(ModelResource):
"""
Class for managing import / export of PurchaseOrder data
"""
# Add number of line items
line_items = Field(attribute='line_count', widget=widgets.IntegerWidget(), readonly=True)
# Is this order overdue?
overdue = Field(attribute='is_overdue', widget=widgets.BooleanWidget(), readonly=True)
class Meta:
model = PurchaseOrder
skip_unchanged = True
clean_model_instances = True
class PurchaseOrderLineItemResource(ModelResource):
""" Class for managing import / export of PurchaseOrderLineItem data """
@ -117,6 +135,23 @@ class PurchaseOrderExtraLineResource(ModelResource):
model = PurchaseOrderExtraLine
class SalesOrderResource(ModelResource):
"""
Class for managing import / export of SalesOrder data
"""
# Add number of line items
line_items = Field(attribute='line_count', widget=widgets.IntegerWidget(), readonly=True)
# Is this order overdue?
overdue = Field(attribute='is_overdue', widget=widgets.BooleanWidget(), readonly=True)
class Meta:
model = SalesOrder
skip_unchanged = True
clean_model_instances = True
class SalesOrderLineItemResource(ModelResource):
"""
Class for managing import / export of SalesOrderLineItem data

View File

@ -17,10 +17,11 @@ from company.models import SupplierPart
from InvenTree.filters import InvenTreeOrderingFilter
from InvenTree.helpers import str2bool, DownloadFile
from InvenTree.api import AttachmentMixin
from InvenTree.api import AttachmentMixin, APIDownloadMixin
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus
from order.admin import PurchaseOrderLineItemResource
from order.admin import PurchaseOrderResource, PurchaseOrderLineItemResource
from order.admin import SalesOrderResource
import order.models as models
import order.serializers as serializers
from part.models import Part
@ -110,7 +111,7 @@ class PurchaseOrderFilter(rest_filters.FilterSet):
]
class PurchaseOrderList(generics.ListCreateAPIView):
class PurchaseOrderList(APIDownloadMixin, generics.ListCreateAPIView):
""" API endpoint for accessing a list of PurchaseOrder objects
- GET: Return list of PurchaseOrder objects (with filters)
@ -160,6 +161,15 @@ class PurchaseOrderList(generics.ListCreateAPIView):
return queryset
def download_queryset(self, queryset, export_format):
dataset = PurchaseOrderResource().export(queryset=queryset)
filedata = dataset.export(export_format)
filename = f"InvenTree_PurchaseOrders.{export_format}"
return DownloadFile(filedata, filename)
def filter_queryset(self, queryset):
# Perform basic filtering
@ -407,7 +417,7 @@ class PurchaseOrderLineItemFilter(rest_filters.FilterSet):
return queryset
class PurchaseOrderLineItemList(generics.ListCreateAPIView):
class PurchaseOrderLineItemList(APIDownloadMixin, generics.ListCreateAPIView):
""" API endpoint for accessing a list of PurchaseOrderLineItem objects
- GET: Return a list of PurchaseOrder Line Item objects
@ -460,25 +470,19 @@ class PurchaseOrderLineItemList(generics.ListCreateAPIView):
return queryset
def download_queryset(self, queryset, export_format):
dataset = PurchaseOrderLineItemResource().export(queryset=queryset)
filedata = dataset.export(export_format)
filename = f"InvenTree_PurchaseOrderItems.{export_format}"
return DownloadFile(filedata, filename)
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
# Check if we wish to export the queried data to a file
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 = PurchaseOrderLineItemResource().export(queryset=queryset)
filedata = dataset.export(export_format)
filename = f"InvenTree_PurchaseOrderData.{export_format}"
return DownloadFile(filedata, filename)
page = self.paginate_queryset(queryset)
if page is not None:
@ -580,7 +584,7 @@ class SalesOrderAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, Attachme
serializer_class = serializers.SalesOrderAttachmentSerializer
class SalesOrderList(generics.ListCreateAPIView):
class SalesOrderList(APIDownloadMixin, generics.ListCreateAPIView):
"""
API endpoint for accessing a list of SalesOrder objects.
@ -630,6 +634,15 @@ class SalesOrderList(generics.ListCreateAPIView):
return queryset
def download_queryset(self, queryset, export_format):
dataset = SalesOrderResource().export(queryset=queryset)
filedata = dataset.export(export_format)
filename = f"InvenTree_SalesOrders.{export_format}"
return DownloadFile(filedata, filename)
def filter_queryset(self, queryset):
"""
Perform custom filtering operations on the SalesOrder queryset.

View File

@ -4,8 +4,8 @@ from __future__ import unicode_literals
from django.contrib import admin
from import_export.admin import ImportExportModelAdmin
from import_export.resources import ModelResource
from import_export.fields import Field
from import_export.resources import ModelResource
import import_export.widgets as widgets
from company.models import SupplierPart

View File

@ -49,7 +49,7 @@ 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.api import AttachmentMixin, APIDownloadMixin
from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus, SalesOrderStatus
@ -847,7 +847,7 @@ class PartFilter(rest_filters.FilterSet):
virtual = rest_filters.BooleanFilter()
class PartList(generics.ListCreateAPIView):
class PartList(APIDownloadMixin, generics.ListCreateAPIView):
"""
API endpoint for accessing a list of Part objects
@ -897,6 +897,14 @@ class PartList(generics.ListCreateAPIView):
return self.serializer_class(*args, **kwargs)
def download_queryset(self, queryset, export_format):
dataset = PartResource().export(queryset=queryset)
filedata = dataset.export(export_format)
filename = f"InvenTree_Parts.{export_format}"
return DownloadFile(filedata, filename)
def list(self, request, *args, **kwargs):
"""
Overide the 'list' method, as the PartCategory objects are
@ -908,22 +916,6 @@ 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:

View File

@ -33,7 +33,7 @@ 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.api import AttachmentMixin, APIDownloadMixin
from InvenTree.filters import InvenTreeOrderingFilter
from order.models import PurchaseOrder
@ -505,7 +505,7 @@ class StockFilter(rest_filters.FilterSet):
updated_after = rest_filters.DateFilter(label='Updated after', field_name='updated', lookup_expr='gte')
class StockList(generics.ListCreateAPIView):
class StockList(APIDownloadMixin, generics.ListCreateAPIView):
""" API endpoint for list view of Stock objects
- GET: Return a list of all StockItem objects (with optional query filters)
@ -646,6 +646,22 @@ class StockList(generics.ListCreateAPIView):
return Response(response_data, status=status.HTTP_201_CREATED, headers=self.get_success_headers(serializer.data))
def download_queryset(self, queryset, export_format):
"""
Download this queryset as a file.
Uses the APIDownloadMixin mixin class
"""
dataset = StockItemResource().export(queryset=queryset)
filedata = dataset.export(export_format)
filename = 'InvenTree_StockItems_{date}.{fmt}'.format(
date=datetime.now().strftime("%d-%b-%Y"),
fmt=export_format
)
return DownloadFile(filedata, filename)
def list(self, request, *args, **kwargs):
"""
Override the 'list' method, as the StockLocation objects
@ -658,25 +674,6 @@ class StockList(generics.ListCreateAPIView):
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:

View File

@ -2355,7 +2355,7 @@ function loadBuildTable(table, options) {
var filterTarget = options.filterTarget || null;
setupFilterList('build', table, filterTarget);
setupFilterList('build', table, filterTarget, {download: true});
$(table).inventreeTable({
method: 'get',

View File

@ -1394,7 +1394,9 @@ function loadPurchaseOrderTable(table, options) {
filters[key] = options.params[key];
}
setupFilterList('purchaseorder', $(table));
var target = '#filter-list-purchaseorder';
setupFilterList('purchaseorder', $(table), target, {download: true});
$(table).inventreeTable({
url: '{% url "api-po-list" %}',
@ -2091,7 +2093,9 @@ function loadSalesOrderTable(table, options) {
options.url = options.url || '{% url "api-so-list" %}';
setupFilterList('salesorder', $(table));
var target = '#filter-list-salesorder';
setupFilterList('salesorder', $(table), target, {download: true});
$(table).inventreeTable({
url: options.url,