2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-29 12:06:44 +00:00

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) 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: class AttachmentMixin:
""" """
Mixin for creating attachment objects, Mixin for creating attachment objects,

View File

@ -4,11 +4,16 @@ InvenTree API version information
# InvenTree API version # 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 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 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 StockItem which does not exist
- Fixes barcode API error response when scanning a StockLocation 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 __future__ import unicode_literals
from django.contrib import admin 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): 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.rest_framework import DjangoFilterBackend
from django_filters import rest_framework as rest_filters from django_filters import rest_framework as rest_filters
from InvenTree.api import AttachmentMixin from InvenTree.api import AttachmentMixin, APIDownloadMixin
from InvenTree.helpers import str2bool, isNull from InvenTree.helpers import str2bool, isNull, DownloadFile
from InvenTree.filters import InvenTreeOrderingFilter from InvenTree.filters import InvenTreeOrderingFilter
from InvenTree.status_codes import BuildStatus from InvenTree.status_codes import BuildStatus
from .models import Build, BuildItem, BuildOrderAttachment import build.admin
import build.serializers import build.serializers
from build.models import Build, BuildItem, BuildOrderAttachment
from users.models import Owner from users.models import Owner
@ -71,7 +73,7 @@ class BuildFilter(rest_filters.FilterSet):
return queryset return queryset
class BuildList(generics.ListCreateAPIView): class BuildList(APIDownloadMixin, generics.ListCreateAPIView):
""" API endpoint for accessing a list of Build objects. """ API endpoint for accessing a list of Build objects.
- GET: Return list of objects (with filters) - GET: Return list of objects (with filters)
@ -123,6 +125,14 @@ class BuildList(generics.ListCreateAPIView):
return queryset 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): def filter_queryset(self, queryset):
queryset = super().filter_queryset(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.admin import ImportExportModelAdmin
from import_export.resources import ModelResource
from import_export.fields import Field 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 PurchaseOrder, PurchaseOrderLineItem, PurchaseOrderExtraLine
from .models import SalesOrder, SalesOrderLineItem, SalesOrderExtraLine from .models import SalesOrder, SalesOrderLineItem, SalesOrderExtraLine
@ -92,6 +93,23 @@ class SalesOrderAdmin(ImportExportModelAdmin):
autocomplete_fields = ('customer',) 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 PurchaseOrderLineItemResource(ModelResource):
""" Class for managing import / export of PurchaseOrderLineItem data """ """ Class for managing import / export of PurchaseOrderLineItem data """
@ -117,6 +135,23 @@ class PurchaseOrderExtraLineResource(ModelResource):
model = PurchaseOrderExtraLine 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 SalesOrderLineItemResource(ModelResource):
""" """
Class for managing import / export of SalesOrderLineItem data 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.filters import InvenTreeOrderingFilter
from InvenTree.helpers import str2bool, DownloadFile 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 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.models as models
import order.serializers as serializers import order.serializers as serializers
from part.models import Part 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 """ API endpoint for accessing a list of PurchaseOrder objects
- GET: Return list of PurchaseOrder objects (with filters) - GET: Return list of PurchaseOrder objects (with filters)
@ -160,6 +161,15 @@ class PurchaseOrderList(generics.ListCreateAPIView):
return queryset 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): def filter_queryset(self, queryset):
# Perform basic filtering # Perform basic filtering
@ -407,7 +417,7 @@ class PurchaseOrderLineItemFilter(rest_filters.FilterSet):
return queryset return queryset
class PurchaseOrderLineItemList(generics.ListCreateAPIView): class PurchaseOrderLineItemList(APIDownloadMixin, generics.ListCreateAPIView):
""" API endpoint for accessing a list of PurchaseOrderLineItem objects """ API endpoint for accessing a list of PurchaseOrderLineItem objects
- GET: Return a list of PurchaseOrder Line Item objects - GET: Return a list of PurchaseOrder Line Item objects
@ -460,25 +470,19 @@ class PurchaseOrderLineItemList(generics.ListCreateAPIView):
return queryset return queryset
def list(self, request, *args, **kwargs): def download_queryset(self, queryset, export_format):
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) dataset = PurchaseOrderLineItemResource().export(queryset=queryset)
filedata = dataset.export(export_format) filedata = dataset.export(export_format)
filename = f"InvenTree_PurchaseOrderData.{export_format}" filename = f"InvenTree_PurchaseOrderItems.{export_format}"
return DownloadFile(filedata, filename) return DownloadFile(filedata, filename)
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset) page = self.paginate_queryset(queryset)
if page is not None: if page is not None:
@ -580,7 +584,7 @@ class SalesOrderAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, Attachme
serializer_class = serializers.SalesOrderAttachmentSerializer serializer_class = serializers.SalesOrderAttachmentSerializer
class SalesOrderList(generics.ListCreateAPIView): class SalesOrderList(APIDownloadMixin, generics.ListCreateAPIView):
""" """
API endpoint for accessing a list of SalesOrder objects. API endpoint for accessing a list of SalesOrder objects.
@ -630,6 +634,15 @@ class SalesOrderList(generics.ListCreateAPIView):
return queryset 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): def filter_queryset(self, queryset):
""" """
Perform custom filtering operations on the SalesOrder 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 django.contrib import admin
from import_export.admin import ImportExportModelAdmin from import_export.admin import ImportExportModelAdmin
from import_export.resources import ModelResource
from import_export.fields import Field from import_export.fields import Field
from import_export.resources import ModelResource
import import_export.widgets as widgets import import_export.widgets as widgets
from company.models import SupplierPart 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 str2bool, isNull, increment
from InvenTree.helpers import DownloadFile from InvenTree.helpers import DownloadFile
from InvenTree.api import AttachmentMixin from InvenTree.api import AttachmentMixin, APIDownloadMixin
from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus, SalesOrderStatus from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus, SalesOrderStatus
@ -847,7 +847,7 @@ class PartFilter(rest_filters.FilterSet):
virtual = rest_filters.BooleanFilter() virtual = rest_filters.BooleanFilter()
class PartList(generics.ListCreateAPIView): class PartList(APIDownloadMixin, generics.ListCreateAPIView):
""" """
API endpoint for accessing a list of Part objects API endpoint for accessing a list of Part objects
@ -897,6 +897,14 @@ class PartList(generics.ListCreateAPIView):
return self.serializer_class(*args, **kwargs) 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): def list(self, request, *args, **kwargs):
""" """
Overide the 'list' method, as the PartCategory objects are Overide the 'list' method, as the PartCategory objects are
@ -908,22 +916,6 @@ 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:

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 str2bool, isNull, extract_serial_numbers
from InvenTree.helpers import DownloadFile from InvenTree.helpers import DownloadFile
from InvenTree.api import AttachmentMixin from InvenTree.api import AttachmentMixin, APIDownloadMixin
from InvenTree.filters import InvenTreeOrderingFilter from InvenTree.filters import InvenTreeOrderingFilter
from order.models import PurchaseOrder 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') 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 """ API endpoint for list view of Stock objects
- GET: Return a list of all StockItem objects (with optional query filters) - 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)) 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): def list(self, request, *args, **kwargs):
""" """
Override the 'list' method, as the StockLocation objects Override the 'list' method, as the StockLocation objects
@ -658,25 +674,6 @@ class StockList(generics.ListCreateAPIView):
params = request.query_params 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:

View File

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

View File

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