mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-30 18:50:53 +00:00
Calendar export (#3858)
* Basic implementation of iCal feed * Add calendar download to API * Improve comments, remove unused outputs * Basic implementation of iCal feed * Add calendar download to API * Improve comments, remove unused outputs * Improve comment * Implement filter include_completed * update requirements.txt with pip-compile --output-file=requirements.txt requirements.in -U * Fix less than filter * Change URL to include calendar.ics * Fix filtering of orders * Remove URL/functions for calendar in views * Lint * More lint * Even more style fixes * Updated requirements-dev.txt because of style fail * Now? * Fine, fix it manually * Gaaah * Fix with same method as in common/settings.py * Fix setting name; improve name of calendar endpoint * Adapt InvenTreeAPITester get function to match post, etc (required for calendar test) * Merge * Reduce requirements.txt * Update requirements-dev.txt * Update tests * Set expected codes in API calendar test * SO completion can not work without line items; set a target date on existing orders instead * Correct method to PATCH * Well that didn't work for some reason.. try with cancelled orders instead * Make sure there are more completed orders than other orders in test * Correct wrong variable * Lint * Use correct status code * Add test for unauthorized access to calendar * Add a working test for unauthorised access * Put the correct test in place, fix Lint * Revert changes to requirements-dev, which appear magically... * Lint * Add test for basic auth * make sample simpler * Increment API version Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
@ -1,17 +1,23 @@
|
||||
"""JSON API for the Order app."""
|
||||
|
||||
from django.contrib.auth import authenticate, login
|
||||
from django.db import transaction
|
||||
from django.db.models import F, Q
|
||||
from django.db.utils import ProgrammingError
|
||||
from django.http.response import JsonResponse
|
||||
from django.urls import include, path, re_path
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from django_filters import rest_framework as rest_filters
|
||||
from django_ical.views import ICalFeed
|
||||
from rest_framework import filters, status
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.response import Response
|
||||
|
||||
import order.models as models
|
||||
import order.serializers as serializers
|
||||
from common.models import InvenTreeSetting
|
||||
from common.settings import settings
|
||||
from company.models import SupplierPart
|
||||
from InvenTree.api import (APIDownloadMixin, AttachmentMixin,
|
||||
ListCreateDestroyAPIView)
|
||||
@ -1168,6 +1174,155 @@ class PurchaseOrderAttachmentDetail(AttachmentMixin, RetrieveUpdateDestroyAPI):
|
||||
serializer_class = serializers.PurchaseOrderAttachmentSerializer
|
||||
|
||||
|
||||
class OrderCalendarExport(ICalFeed):
|
||||
"""Calendar export for Purchase/Sales Orders
|
||||
|
||||
Optional parameters:
|
||||
- include_completed: true/false
|
||||
whether or not to show completed orders. Defaults to false
|
||||
"""
|
||||
|
||||
try:
|
||||
instance_url = InvenTreeSetting.get_setting('INVENTREE_BASE_URL', create=False, cache=False)
|
||||
except ProgrammingError: # pragma: no cover
|
||||
# database is not initialized yet
|
||||
instance_url = ''
|
||||
instance_url = instance_url.replace("http://", "").replace("https://", "")
|
||||
timezone = settings.TIME_ZONE
|
||||
file_name = "calendar.ics"
|
||||
|
||||
def __call__(self, request, *args, **kwargs):
|
||||
"""Overload call in order to check for authentication.
|
||||
|
||||
This is required to force Django to look for the authentication,
|
||||
otherwise login request with Basic auth via curl or similar are ignored,
|
||||
and login via a calendar client will not work.
|
||||
|
||||
See:
|
||||
https://stackoverflow.com/questions/3817694/django-rss-feed-authentication
|
||||
https://stackoverflow.com/questions/152248/can-i-use-http-basic-authentication-with-django
|
||||
https://www.djangosnippets.org/snippets/243/
|
||||
"""
|
||||
|
||||
import base64
|
||||
|
||||
if request.user.is_authenticated:
|
||||
# Authenticated on first try - maybe normal browser call?
|
||||
return super().__call__(request, *args, **kwargs)
|
||||
|
||||
# No login yet - check in headers
|
||||
if 'HTTP_AUTHORIZATION' in request.META:
|
||||
auth = request.META['HTTP_AUTHORIZATION'].split()
|
||||
if len(auth) == 2:
|
||||
# NOTE: We are only support basic authentication for now.
|
||||
#
|
||||
if auth[0].lower() == "basic":
|
||||
uname, passwd = base64.b64decode(auth[1]).decode("ascii").split(':')
|
||||
user = authenticate(username=uname, password=passwd)
|
||||
if user is not None:
|
||||
if user.is_active:
|
||||
login(request, user)
|
||||
request.user = user
|
||||
|
||||
# Check again
|
||||
if request.user.is_authenticated:
|
||||
# Authenticated after second try
|
||||
return super().__call__(request, *args, **kwargs)
|
||||
|
||||
# Still nothing - return Unauth. header with info on how to authenticate
|
||||
# Information is needed by client, eg Thunderbird
|
||||
response = JsonResponse({"detail": "Authentication credentials were not provided."})
|
||||
response['WWW-Authenticate'] = 'Basic realm="api"'
|
||||
response.status_code = 401
|
||||
return response
|
||||
|
||||
def get_object(self, request, *args, **kwargs):
|
||||
"""This is where settings from the URL etc will be obtained"""
|
||||
# Help:
|
||||
# https://django.readthedocs.io/en/stable/ref/contrib/syndication.html
|
||||
|
||||
obj = dict()
|
||||
obj['ordertype'] = kwargs['ordertype']
|
||||
obj['include_completed'] = bool(request.GET.get('include_completed', False))
|
||||
|
||||
return obj
|
||||
|
||||
def title(self, obj):
|
||||
"""Return calendar title."""
|
||||
|
||||
if obj["ordertype"] == 'purchase-order':
|
||||
ordertype_title = _('Purchase Order')
|
||||
elif obj["ordertype"] == 'sales-order':
|
||||
ordertype_title = _('Sales Order')
|
||||
else:
|
||||
ordertype_title = _('Unknown')
|
||||
|
||||
return f'{InvenTreeSetting.get_setting("INVENTREE_COMPANY_NAME")} {ordertype_title}'
|
||||
|
||||
def product_id(self, obj):
|
||||
"""Return calendar product id."""
|
||||
return f'//{self.instance_url}//{self.title(obj)}//EN'
|
||||
|
||||
def items(self, obj):
|
||||
"""Return a list of PurchaseOrders.
|
||||
|
||||
Filters:
|
||||
- Only return those which have a target_date set
|
||||
- Only return incomplete orders, unless include_completed is set to True
|
||||
"""
|
||||
if obj['ordertype'] == 'purchase-order':
|
||||
if obj['include_completed'] is False:
|
||||
# Do not include completed orders from list in this case
|
||||
# Completed status = 30
|
||||
outlist = models.PurchaseOrder.objects.filter(target_date__isnull=False).filter(status__lt=PurchaseOrderStatus.COMPLETE)
|
||||
else:
|
||||
outlist = models.PurchaseOrder.objects.filter(target_date__isnull=False)
|
||||
else:
|
||||
if obj['include_completed'] is False:
|
||||
# Do not include completed (=shipped) orders from list in this case
|
||||
# Shipped status = 20
|
||||
outlist = models.SalesOrder.objects.filter(target_date__isnull=False).filter(status__lt=SalesOrderStatus.SHIPPED)
|
||||
else:
|
||||
outlist = models.SalesOrder.objects.filter(target_date__isnull=False)
|
||||
|
||||
return outlist
|
||||
|
||||
def item_title(self, item):
|
||||
"""Set the event title to the purchase order reference"""
|
||||
return item.reference
|
||||
|
||||
def item_description(self, item):
|
||||
"""Set the event description"""
|
||||
return item.description
|
||||
|
||||
def item_start_datetime(self, item):
|
||||
"""Set event start to target date. Goal is all-day event."""
|
||||
return item.target_date
|
||||
|
||||
def item_end_datetime(self, item):
|
||||
"""Set event end to target date. Goal is all-day event."""
|
||||
return item.target_date
|
||||
|
||||
def item_created(self, item):
|
||||
"""Use creation date of PO as creation date of event."""
|
||||
return item.creation_date
|
||||
|
||||
def item_class(self, item):
|
||||
"""Set item class to PUBLIC"""
|
||||
return 'PUBLIC'
|
||||
|
||||
def item_guid(self, item):
|
||||
"""Return globally unique UID for event"""
|
||||
return f'po_{item.pk}_{item.reference.replace(" ","-")}@{self.instance_url}'
|
||||
|
||||
def item_link(self, item):
|
||||
"""Set the item link."""
|
||||
|
||||
# Do not use instance_url as here, as the protocol needs to be included
|
||||
site_url = InvenTreeSetting.get_setting("INVENTREE_BASE_URL")
|
||||
return f'{site_url}{item.get_absolute_url()}'
|
||||
|
||||
|
||||
order_api_urls = [
|
||||
|
||||
# API endpoints for purchase orders
|
||||
@ -1255,4 +1410,7 @@ order_api_urls = [
|
||||
path('<int:pk>/', SalesOrderAllocationDetail.as_view(), name='api-so-allocation-detail'),
|
||||
re_path(r'^.*$', SalesOrderAllocationList.as_view(), name='api-so-allocation-list'),
|
||||
])),
|
||||
|
||||
# API endpoint for subscribing to ICS calendar of purchase/sales orders
|
||||
re_path(r'^calendar/(?P<ordertype>purchase-order|sales-order)/calendar.ics', OrderCalendarExport(), name='api-po-so-calendar'),
|
||||
]
|
||||
|
Reference in New Issue
Block a user