mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-03 20:20:58 +00:00
* Adds ReturnOrder and ReturnOrderAttachment models
* Adds new 'role' specific for return orders
* Refactor total_price into a mixin
- Required for PurchaseOrder and SalesOrder
- May not be required for ReturnOrder (remains to be seen)
* Adds API endpoints for ReturnOrder
- Add list endpoint
- Add detail endpoint
- Adds required serializer models
* Adds basic "index" page for Return Order model
* Update API version
* Update navbar text
* Add db migration for new "role"
* Add ContactList and ContactDetail API endpoints
* Adds template and JS code for manipulation of contacts
- Display a table
- Create / edit / delete
* Splits order.js into multiple files
- Javascript files was becoming extremely large
- Hard to debug and find code
- Split into purchase_order / return_order / sales_order
* Fix role name (change 'returns' to 'return_order')
- Similar to existing roles for purchase_order and sales_order
* Adds detail page for ReturnOrder
* URL cleanup
- Use <int:pk> instead of complex regex
* More URL cleanup
* Add "return orders" list to company detail page
* Break JS status codes into new javascript file
- Always difficult to track down where these are rendered
- Enough to warrant their own file now
* Add ability to edit return order from detail page
* Database migrations
- Add new ReturnOrder modeles
- Add new 'contact' field to external orders
* Adds "contact" to ReturnOrder
- Implement check to ensure that the selected "contact" matches the selected "company"
* Adjust filters to limit contact options
* Fix typo
* Expose 'contact' field for PurchaseOrder model
* Render contact information
* Add "contact" for SalesOrder
* Adds setting to enable / disable return order functionality
- Simply hides the navigation elements
- API is not disabled
* Support filtering ReturnOrder by 'status'
- Refactors existing filter into the OrderFilter class
* js linting
* More JS linting
* Adds ReturnOrderReport model
* Add serializer for the ReturnOrderReport model
- A little bit of refactoring along the way
* Admin integration for new report model
* Refactoring for report.api
- Adds generic mixins for filtering queryset (based on updates to label.api)
- Reduces repeated code a *lot*
* Exposes API endpoints for ReturnOrderReport
* Adds default example report file for ReturnOrder
- Requires some more work :)
* Refactor report printing javascript code
- Replace all existing functions with 'printReports'
* Improvements for default StockItem test report template
- Fix bug in template
- Handle potential errors in template tags
- Add more helpers to report tags
- Improve test result rendering
* Reduce logging verbosity from weasyprint
* Refactor javascript for label printing
- Consolidate into a single function
- Similar to refactor of report functions
* Add report print button to return order page
* Record user reference when creating via API
* Refactor order serializers
- Move common code into AbstractOrderSerializer class
* Adds extra line item model for the return order
- Adds serializer and API endpoints as appropriate
* Render extra line table for return order
- Refactor existing functions into a single generic function
- Reduces repeated JS code a lot
* Add ability to create a new extra line item
* Adds button for creating a new lien item
* JS linting
* Update test
* Typo fix
(cherry picked from commit 28ac2be35b
)
* Enable search for return order
* Don't do pricing (yet) for returnorder extra line table
- Fixes an uncaught error
* Error catching for api.js
* Updates for order models:
- Add 'target_date' field to abstract Order model
- Add IN_PROGRESS status code for return order
- Refactor 'overdue' and 'outstanding' API queries
- Refactor OVERDUE_FILTER on order models
- Refactor is_overdue on order models
- More table filters for return order model
* JS cleanup
* Create ReturnOrderLineItem model
- New type of status label
- Add TotalPriceMixin to ReturnOrder model
* Adds an API serializer for the ReturnOrderLineItem model
* Add API endpoints for ReturnOrderLineItem model
- Including some refactoring along the way
* javascript: refactor loadTableFilters function
- Pass enforced query through to the filters
- Call Object.assign() to construct a superset query
- Removes a lot of code duplication
* Refactor hard-coded URLS to use {% url %} lookup
- Forces error if the URL is wrong
- If we ever change the URL, will still work
* Implement creation of new return order line items
* Adds 'part_detail' annotation to ReturnOrderLineItem serializer
- Required for rendering part information
* javascript: refactor method for creating a group of buttons in a table
* javascript: refactor common buttons with helper functions
* Allow edit and delete of return order line items
* Add form option to automatically reload a table on success
- Pass table name to options.refreshTable
* JS linting
* Add common function for createExtraLineItem
* Refactor loading of attachment tables
- Setup drag-and-drop as part of core function
* CI fixes
* Refactoring out some more common API endpoint code
* Update migrations
* Fix permission typo
* Refactor for unit testing code
* Add unit tests for Contact model
* Tests for returnorder list API
* Annotate 'line_items' to ReturnOrder serializer
* Driving the refactor tractor
* More unit tests for the ReturnOrder API endpoints
* Refactor "print orders" button for various order tables
- Move into "setupFilterList" code (generic)
* add generic 'label printing' button to table actions buttons
* Refactor build output table
* Refactoring icon generation for js
* Refactoring for Part API
* Fix database model type for 'received_date'
* Add API endpoint to "issue" a ReturnOrder
* Improvements for stock tracking table
- Add new status codes
- Add rendering for SalesOrder
- Add rendering for ReturnOrder
- Fix status badges
* Adds functionality to receive line items against a return order
* Add endpoints for completing and cancelling orders
* Add option to allow / prevent editing of ReturnOrder after completed
* js linting
* Wrap "add extra line" button in setting check
* Updates to order/admin.py
* Remove inline admin for returnorderline model
* Updates to pass CI
* Serializer fix
* order template fixes
* Unit test fix
* Fixes for ReturnOrder.receive_line_item
* Unit testing for receiving line items against an RMA
* Improve example report for return order
* Extend unit tests for reporting
* Cleanup here and there
* Unit testing for order views
* Clear "sales_order" field when returning against ReturnOrder
* Add 'location' to deltas when returning from customer
* Bug fix for unit test
514 lines
16 KiB
Python
514 lines
16 KiB
Python
"""Provides a JSON API for common components."""
|
|
|
|
import json
|
|
|
|
from django.http.response import HttpResponse
|
|
from django.urls import include, path, re_path
|
|
from django.utils.decorators import method_decorator
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
|
|
from django_filters.rest_framework import DjangoFilterBackend
|
|
from django_q.tasks import async_task
|
|
from djmoney.contrib.exchange.models import ExchangeBackend, Rate
|
|
from rest_framework import filters, permissions, serializers
|
|
from rest_framework.exceptions import NotAcceptable, NotFound
|
|
from rest_framework.permissions import IsAdminUser
|
|
from rest_framework.response import Response
|
|
from rest_framework.views import APIView
|
|
|
|
import common.models
|
|
import common.serializers
|
|
from InvenTree.api import BulkDeleteMixin
|
|
from InvenTree.config import CONFIG_LOOKUPS
|
|
from InvenTree.helpers import inheritors
|
|
from InvenTree.mixins import (ListAPI, RetrieveAPI, RetrieveUpdateAPI,
|
|
RetrieveUpdateDestroyAPI)
|
|
from InvenTree.permissions import IsSuperuser
|
|
from plugin.models import NotificationUserSetting
|
|
from plugin.serializers import NotificationUserSettingSerializer
|
|
|
|
|
|
class CsrfExemptMixin(object):
|
|
"""Exempts the view from CSRF requirements."""
|
|
|
|
@method_decorator(csrf_exempt)
|
|
def dispatch(self, *args, **kwargs):
|
|
"""Overwrites dispatch to be extempt from csrf checks."""
|
|
return super().dispatch(*args, **kwargs)
|
|
|
|
|
|
class WebhookView(CsrfExemptMixin, APIView):
|
|
"""Endpoint for receiving webhooks."""
|
|
authentication_classes = []
|
|
permission_classes = []
|
|
model_class = common.models.WebhookEndpoint
|
|
run_async = False
|
|
|
|
def post(self, request, endpoint, *args, **kwargs):
|
|
"""Process incomming webhook."""
|
|
# get webhook definition
|
|
self._get_webhook(endpoint, request, *args, **kwargs)
|
|
|
|
# check headers
|
|
headers = request.headers
|
|
try:
|
|
payload = json.loads(request.body)
|
|
except json.decoder.JSONDecodeError as error:
|
|
raise NotAcceptable(error.msg)
|
|
|
|
# validate
|
|
self.webhook.validate_token(payload, headers, request)
|
|
# process data
|
|
message = self.webhook.save_data(payload, headers, request)
|
|
if self.run_async:
|
|
async_task(self._process_payload, message.id)
|
|
else:
|
|
self._process_result(
|
|
self.webhook.process_payload(message, payload, headers),
|
|
message,
|
|
)
|
|
|
|
data = self.webhook.get_return(payload, headers, request)
|
|
return HttpResponse(data)
|
|
|
|
def _process_payload(self, message_id):
|
|
message = common.models.WebhookMessage.objects.get(message_id=message_id)
|
|
self._process_result(
|
|
self.webhook.process_payload(message, message.body, message.header),
|
|
message,
|
|
)
|
|
|
|
def _process_result(self, result, message):
|
|
if result:
|
|
message.worked_on = result
|
|
message.save()
|
|
else:
|
|
message.delete()
|
|
|
|
def _escalate_object(self, obj):
|
|
classes = inheritors(obj.__class__)
|
|
for cls in classes:
|
|
mdl_name = cls._meta.model_name
|
|
if hasattr(obj, mdl_name):
|
|
return getattr(obj, mdl_name)
|
|
return obj
|
|
|
|
def _get_webhook(self, endpoint, request, *args, **kwargs):
|
|
try:
|
|
webhook = self.model_class.objects.get(endpoint_id=endpoint)
|
|
self.webhook = self._escalate_object(webhook)
|
|
self.webhook.init(request, *args, **kwargs)
|
|
return self.webhook.process_webhook()
|
|
except self.model_class.DoesNotExist:
|
|
raise NotFound()
|
|
|
|
|
|
class CurrencyExchangeView(APIView):
|
|
"""API endpoint for displaying currency information"""
|
|
|
|
permission_classes = [
|
|
permissions.IsAuthenticated,
|
|
]
|
|
|
|
def get(self, request, format=None):
|
|
"""Return information on available currency conversions"""
|
|
|
|
# Extract a list of all available rates
|
|
try:
|
|
rates = Rate.objects.all()
|
|
except Exception:
|
|
rates = []
|
|
|
|
# Information on last update
|
|
try:
|
|
backend = ExchangeBackend.objects.get(name='InvenTreeExchange')
|
|
updated = backend.last_update
|
|
except Exception:
|
|
updated = None
|
|
|
|
response = {
|
|
'base_currency': common.models.InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY', 'USD'),
|
|
'exchange_rates': {},
|
|
'updated': updated,
|
|
}
|
|
|
|
for rate in rates:
|
|
response['exchange_rates'][rate.currency] = rate.value
|
|
|
|
return Response(response)
|
|
|
|
|
|
class CurrencyRefreshView(APIView):
|
|
"""API endpoint for manually refreshing currency exchange rates.
|
|
|
|
User must be a 'staff' user to access this endpoint
|
|
"""
|
|
|
|
permission_classes = [
|
|
permissions.IsAuthenticated,
|
|
permissions.IsAdminUser,
|
|
]
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
"""Performing a POST request will update currency exchange rates"""
|
|
|
|
from InvenTree.tasks import update_exchange_rates
|
|
|
|
update_exchange_rates()
|
|
|
|
return Response({
|
|
'success': 'Exchange rates updated',
|
|
})
|
|
|
|
|
|
class SettingsList(ListAPI):
|
|
"""Generic ListView for settings.
|
|
|
|
This is inheritted by all list views for settings.
|
|
"""
|
|
|
|
filter_backends = [
|
|
DjangoFilterBackend,
|
|
filters.SearchFilter,
|
|
filters.OrderingFilter,
|
|
]
|
|
|
|
ordering_fields = [
|
|
'pk',
|
|
'key',
|
|
'name',
|
|
]
|
|
|
|
search_fields = [
|
|
'key',
|
|
]
|
|
|
|
|
|
class GlobalSettingsList(SettingsList):
|
|
"""API endpoint for accessing a list of global settings objects."""
|
|
|
|
queryset = common.models.InvenTreeSetting.objects.exclude(key__startswith="_")
|
|
serializer_class = common.serializers.GlobalSettingsSerializer
|
|
|
|
|
|
class GlobalSettingsPermissions(permissions.BasePermission):
|
|
"""Special permission class to determine if the user is "staff"."""
|
|
|
|
def has_permission(self, request, view):
|
|
"""Check that the requesting user is 'admin'."""
|
|
try:
|
|
user = request.user
|
|
|
|
if request.method in ['GET', 'HEAD', 'OPTIONS']:
|
|
return True
|
|
else:
|
|
# Any other methods require staff access permissions
|
|
return user.is_staff
|
|
|
|
except AttributeError: # pragma: no cover
|
|
return False
|
|
|
|
|
|
class GlobalSettingsDetail(RetrieveUpdateAPI):
|
|
"""Detail view for an individual "global setting" object.
|
|
|
|
- User must have 'staff' status to view / edit
|
|
"""
|
|
|
|
lookup_field = 'key'
|
|
queryset = common.models.InvenTreeSetting.objects.exclude(key__startswith="_")
|
|
serializer_class = common.serializers.GlobalSettingsSerializer
|
|
|
|
def get_object(self):
|
|
"""Attempt to find a global setting object with the provided key."""
|
|
key = self.kwargs['key']
|
|
|
|
if key not in common.models.InvenTreeSetting.SETTINGS.keys():
|
|
raise NotFound()
|
|
|
|
return common.models.InvenTreeSetting.get_setting_object(key)
|
|
|
|
permission_classes = [
|
|
permissions.IsAuthenticated,
|
|
GlobalSettingsPermissions,
|
|
]
|
|
|
|
|
|
class UserSettingsList(SettingsList):
|
|
"""API endpoint for accessing a list of user settings objects."""
|
|
|
|
queryset = common.models.InvenTreeUserSetting.objects.all()
|
|
serializer_class = common.serializers.UserSettingsSerializer
|
|
|
|
def filter_queryset(self, queryset):
|
|
"""Only list settings which apply to the current user."""
|
|
try:
|
|
user = self.request.user
|
|
except AttributeError: # pragma: no cover
|
|
return common.models.InvenTreeUserSetting.objects.none()
|
|
|
|
queryset = super().filter_queryset(queryset)
|
|
|
|
queryset = queryset.filter(user=user)
|
|
|
|
return queryset
|
|
|
|
|
|
class UserSettingsPermissions(permissions.BasePermission):
|
|
"""Special permission class to determine if the user can view / edit a particular setting."""
|
|
|
|
def has_object_permission(self, request, view, obj):
|
|
"""Check if the user that requested is also the object owner."""
|
|
try:
|
|
user = request.user
|
|
except AttributeError: # pragma: no cover
|
|
return False
|
|
|
|
return user == obj.user
|
|
|
|
|
|
class UserSettingsDetail(RetrieveUpdateAPI):
|
|
"""Detail view for an individual "user setting" object.
|
|
|
|
- User can only view / edit settings their own settings objects
|
|
"""
|
|
|
|
lookup_field = 'key'
|
|
queryset = common.models.InvenTreeUserSetting.objects.all()
|
|
serializer_class = common.serializers.UserSettingsSerializer
|
|
|
|
def get_object(self):
|
|
"""Attempt to find a user setting object with the provided key."""
|
|
key = self.kwargs['key']
|
|
|
|
if key not in common.models.InvenTreeUserSetting.SETTINGS.keys():
|
|
raise NotFound()
|
|
|
|
return common.models.InvenTreeUserSetting.get_setting_object(key, user=self.request.user)
|
|
|
|
permission_classes = [
|
|
UserSettingsPermissions,
|
|
]
|
|
|
|
|
|
class NotificationUserSettingsList(SettingsList):
|
|
"""API endpoint for accessing a list of notification user settings objects."""
|
|
|
|
queryset = NotificationUserSetting.objects.all()
|
|
serializer_class = NotificationUserSettingSerializer
|
|
|
|
def filter_queryset(self, queryset):
|
|
"""Only list settings which apply to the current user."""
|
|
try:
|
|
user = self.request.user
|
|
except AttributeError:
|
|
return NotificationUserSetting.objects.none()
|
|
|
|
queryset = super().filter_queryset(queryset)
|
|
queryset = queryset.filter(user=user)
|
|
return queryset
|
|
|
|
|
|
class NotificationUserSettingsDetail(RetrieveUpdateAPI):
|
|
"""Detail view for an individual "notification user setting" object.
|
|
|
|
- User can only view / edit settings their own settings objects
|
|
"""
|
|
|
|
queryset = NotificationUserSetting.objects.all()
|
|
serializer_class = NotificationUserSettingSerializer
|
|
permission_classes = [UserSettingsPermissions, ]
|
|
|
|
|
|
class NotificationMessageMixin:
|
|
"""Generic mixin for NotificationMessage."""
|
|
queryset = common.models.NotificationMessage.objects.all()
|
|
serializer_class = common.serializers.NotificationMessageSerializer
|
|
permission_classes = [UserSettingsPermissions, ]
|
|
|
|
|
|
class NotificationList(NotificationMessageMixin, BulkDeleteMixin, ListAPI):
|
|
"""List view for all notifications of the current user."""
|
|
|
|
permission_classes = [permissions.IsAuthenticated, ]
|
|
|
|
filter_backends = [
|
|
DjangoFilterBackend,
|
|
filters.SearchFilter,
|
|
filters.OrderingFilter,
|
|
]
|
|
|
|
ordering_fields = [
|
|
'category',
|
|
'name',
|
|
'read',
|
|
'creation',
|
|
]
|
|
|
|
search_fields = [
|
|
'name',
|
|
'message',
|
|
]
|
|
|
|
filterset_fields = [
|
|
'category',
|
|
'read',
|
|
]
|
|
|
|
def filter_queryset(self, queryset):
|
|
"""Only list notifications which apply to the current user."""
|
|
try:
|
|
user = self.request.user
|
|
except AttributeError:
|
|
return common.models.NotificationMessage.objects.none()
|
|
|
|
queryset = super().filter_queryset(queryset)
|
|
queryset = queryset.filter(user=user)
|
|
return queryset
|
|
|
|
def filter_delete_queryset(self, queryset, request):
|
|
"""Ensure that the user can only delete their *own* notifications"""
|
|
|
|
queryset = queryset.filter(user=request.user)
|
|
return queryset
|
|
|
|
|
|
class NotificationDetail(NotificationMessageMixin, RetrieveUpdateDestroyAPI):
|
|
"""Detail view for an individual notification object.
|
|
|
|
- User can only view / delete their own notification objects
|
|
"""
|
|
|
|
|
|
class NotificationReadAll(NotificationMessageMixin, RetrieveAPI):
|
|
"""API endpoint to mark all notifications as read."""
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
"""Set all messages for the current user as read."""
|
|
try:
|
|
self.queryset.filter(user=request.user, read=False).update(read=True)
|
|
return Response({'status': 'ok'})
|
|
except Exception as exc:
|
|
raise serializers.ValidationError(detail=serializers.as_serializer_error(exc))
|
|
|
|
|
|
class NewsFeedMixin:
|
|
"""Generic mixin for NewsFeedEntry."""
|
|
queryset = common.models.NewsFeedEntry.objects.all()
|
|
serializer_class = common.serializers.NewsFeedEntrySerializer
|
|
permission_classes = [IsAdminUser, ]
|
|
|
|
|
|
class NewsFeedEntryList(NewsFeedMixin, BulkDeleteMixin, ListAPI):
|
|
"""List view for all news items."""
|
|
filter_backends = [
|
|
DjangoFilterBackend,
|
|
filters.OrderingFilter,
|
|
]
|
|
|
|
ordering_fields = [
|
|
'published',
|
|
'author',
|
|
'read',
|
|
]
|
|
|
|
filterset_fields = [
|
|
'read',
|
|
]
|
|
|
|
|
|
class NewsFeedEntryDetail(NewsFeedMixin, RetrieveUpdateDestroyAPI):
|
|
"""Detail view for an individual news feed object."""
|
|
|
|
|
|
class ConfigList(ListAPI):
|
|
"""List view for all accessed configurations."""
|
|
|
|
queryset = CONFIG_LOOKUPS
|
|
serializer_class = common.serializers.ConfigSerializer
|
|
permission_classes = [IsSuperuser, ]
|
|
|
|
|
|
class ConfigDetail(RetrieveAPI):
|
|
"""Detail view for an individual configuration."""
|
|
|
|
serializer_class = common.serializers.ConfigSerializer
|
|
permission_classes = [IsSuperuser, ]
|
|
|
|
def get_object(self):
|
|
"""Attempt to find a config object with the provided key."""
|
|
key = self.kwargs['key']
|
|
value = CONFIG_LOOKUPS.get(key, None)
|
|
if not value:
|
|
raise NotFound()
|
|
return {key: value}
|
|
|
|
|
|
settings_api_urls = [
|
|
# User settings
|
|
re_path(r'^user/', include([
|
|
# User Settings Detail
|
|
re_path(r'^(?P<key>\w+)/', UserSettingsDetail.as_view(), name='api-user-setting-detail'),
|
|
|
|
# User Settings List
|
|
re_path(r'^.*$', UserSettingsList.as_view(), name='api-user-setting-list'),
|
|
])),
|
|
|
|
# Notification settings
|
|
re_path(r'^notification/', include([
|
|
# Notification Settings Detail
|
|
path(r'<int:pk>/', NotificationUserSettingsDetail.as_view(), name='api-notification-setting-detail'),
|
|
|
|
# Notification Settings List
|
|
re_path(r'^.*$', NotificationUserSettingsList.as_view(), name='api-notifcation-setting-list'),
|
|
])),
|
|
|
|
# Global settings
|
|
re_path(r'^global/', include([
|
|
# Global Settings Detail
|
|
re_path(r'^(?P<key>\w+)/', GlobalSettingsDetail.as_view(), name='api-global-setting-detail'),
|
|
|
|
# Global Settings List
|
|
re_path(r'^.*$', GlobalSettingsList.as_view(), name='api-global-setting-list'),
|
|
])),
|
|
]
|
|
|
|
common_api_urls = [
|
|
# Webhooks
|
|
path('webhook/<slug:endpoint>/', WebhookView.as_view(), name='api-webhook'),
|
|
|
|
# Currencies
|
|
re_path(r'^currency/', include([
|
|
re_path(r'^exchange/', CurrencyExchangeView.as_view(), name='api-currency-exchange'),
|
|
re_path(r'^refresh/', CurrencyRefreshView.as_view(), name='api-currency-refresh'),
|
|
])),
|
|
|
|
# Notifications
|
|
re_path(r'^notifications/', include([
|
|
# Individual purchase order detail URLs
|
|
path(r'<int:pk>/', include([
|
|
re_path(r'.*$', NotificationDetail.as_view(), name='api-notifications-detail'),
|
|
])),
|
|
# Read all
|
|
re_path(r'^readall/', NotificationReadAll.as_view(), name='api-notifications-readall'),
|
|
|
|
# Notification messages list
|
|
re_path(r'^.*$', NotificationList.as_view(), name='api-notifications-list'),
|
|
])),
|
|
|
|
# News
|
|
re_path(r'^news/', include([
|
|
path(r'<int:pk>/', include([
|
|
re_path(r'.*$', NewsFeedEntryDetail.as_view(), name='api-news-detail'),
|
|
])),
|
|
re_path(r'^.*$', NewsFeedEntryList.as_view(), name='api-news-list'),
|
|
])),
|
|
|
|
]
|
|
|
|
admin_api_urls = [
|
|
# Admin
|
|
path('config/', ConfigList.as_view(), name='api-config-list'),
|
|
path('config/<str:key>/', ConfigDetail.as_view(), name='api-config-detail'),
|
|
]
|