2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-28 03:26:45 +00:00
Oliver 3afafe594b
[UI] Reimplement calendar views (#8933)
* Add deps for @fullcalendar

* Basic calendar setup

* Add locale support

* Add custom <SegmentedIconControl> component

* optional 'control' for panel

* Scaffolding out build index page

* Fetch build orders based on calendar dates

* Display builds

* Click to navigate to build

* Working on custom header

* Implement button functionality

* Display current month name

* Implement datepicker dropdown

* Adjust layout

* Custom rendering

* Include part detail

* Implement hook to track calendar state

* Clearing search causes immediate update

* Offload core functionality into hook

- Adds search input

* Spread props correctly

* Cleanup

* Allow build orders to be edited directly from calendar view

* Cleanup

* Fix for InvenTreeCalendarProps

* Improve date range filtering for Build model

* Simplify BuildFilter

* Refactoring

* PurchaseOrder calendar implementation

* Allow edit of purchase orders

* Refactor and implement for all order types

- Add missing "issue_date" field for SalesOrder
- Refactor API for other order types
- Fix rendering issues

* Bump API version

* Add playwright tests for calendar (simple)

* Fix migrations

* Simplify notifications

* Refactor filter components

- Make them more generic

* Enable custom filters for calendars

* Custom filters per table

* Display status label

* Refactoring

* Update playwright tests

* Update build order docs

* Bug fix for order date editing

* Update purchase order docs

* Return Order docs

* Update date formatting
2025-03-16 18:01:51 +11:00

2094 lines
68 KiB
Python

"""JSON API for the Order app."""
from decimal import Decimal
from typing import cast
from django.conf import settings
from django.contrib.auth import authenticate, login
from django.contrib.auth.models import User
from django.db.models import F, Q
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 status
from rest_framework.response import Response
import common.models
import common.settings
import company.models
from generic.states.api import StatusView
from importer.mixins import DataExportViewMixin
from InvenTree.api import ListCreateDestroyAPIView, MetadataView
from InvenTree.filters import (
SEARCH_ORDER_FILTER,
SEARCH_ORDER_FILTER_ALIAS,
InvenTreeDateFilter,
)
from InvenTree.helpers import str2bool
from InvenTree.helpers_model import construct_absolute_url, get_base_url
from InvenTree.mixins import CreateAPI, ListAPI, ListCreateAPI, RetrieveUpdateDestroyAPI
from order import models, serializers
from order.status_codes import (
PurchaseOrderStatus,
PurchaseOrderStatusGroups,
ReturnOrderLineStatus,
ReturnOrderStatus,
SalesOrderStatus,
SalesOrderStatusGroups,
)
from part.models import Part
from users.models import Owner
class GeneralExtraLineList(DataExportViewMixin):
"""General template for ExtraLine API classes."""
def get_serializer(self, *args, **kwargs):
"""Return the serializer instance for this endpoint."""
try:
params = self.request.query_params3
kwargs['order_detail'] = str2bool(params.get('order_detail', False))
except AttributeError:
pass
kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs)
def get_queryset(self, *args, **kwargs):
"""Return the annotated queryset for this endpoint."""
queryset = super().get_queryset(*args, **kwargs)
queryset = queryset.prefetch_related('order')
return queryset
filter_backends = SEARCH_ORDER_FILTER
ordering_fields = ['quantity', 'notes', 'reference']
search_fields = ['quantity', 'notes', 'reference', 'description']
filterset_fields = ['order']
class OrderCreateMixin:
"""Mixin class which handles order creation via API."""
def create(self, request, *args, **kwargs):
"""Save user information on order creation."""
serializer = self.get_serializer(data=self.clean_data(request.data))
serializer.is_valid(raise_exception=True)
item = serializer.save()
item.created_by = request.user
item.save()
headers = self.get_success_headers(serializer.data)
return Response(
serializer.data, status=status.HTTP_201_CREATED, headers=headers
)
class OrderFilter(rest_filters.FilterSet):
"""Base class for custom API filters for the OrderList endpoint."""
# Filter against order status
status = rest_filters.NumberFilter(label=_('Order Status'), method='filter_status')
def filter_status(self, queryset, name, value):
"""Filter by integer status code.
Note: Also account for the possibility of a custom status code.
"""
q1 = Q(status=value, status_custom_key__isnull=True)
q2 = Q(status_custom_key=value)
return queryset.filter(q1 | q2).distinct()
# Exact match for reference
reference = rest_filters.CharFilter(
label=_('Order Reference'), field_name='reference', lookup_expr='iexact'
)
assigned_to_me = rest_filters.BooleanFilter(
label=_('Assigned to me'), method='filter_assigned_to_me'
)
def filter_assigned_to_me(self, queryset, name, value):
"""Filter by orders which are assigned to the current user."""
# Work out who "me" is!
owners = Owner.get_owners_matching_user(self.request.user)
if str2bool(value):
return queryset.filter(responsible__in=owners)
return queryset.exclude(responsible__in=owners)
overdue = rest_filters.BooleanFilter(label='overdue', method='filter_overdue')
def filter_overdue(self, queryset, name, value):
"""Generic filter for determining if an order is 'overdue'.
Note that the overdue_filter() classmethod must be defined for the model
"""
if str2bool(value):
return queryset.filter(self.Meta.model.overdue_filter())
return queryset.exclude(self.Meta.model.overdue_filter())
outstanding = rest_filters.BooleanFilter(
label=_('Outstanding'), method='filter_outstanding'
)
def filter_outstanding(self, queryset, name, value):
"""Generic filter for determining if an order is 'outstanding'."""
if str2bool(value):
return queryset.filter(status__in=self.Meta.model.get_status_class().OPEN)
return queryset.exclude(status__in=self.Meta.model.get_status_class().OPEN)
project_code = rest_filters.ModelChoiceFilter(
queryset=common.models.ProjectCode.objects.all(),
field_name='project_code',
label=_('Project Code'),
)
has_project_code = rest_filters.BooleanFilter(
method='filter_has_project_code', label=_('Has Project Code')
)
def filter_has_project_code(self, queryset, name, value):
"""Filter by whether or not the order has a project code."""
if str2bool(value):
return queryset.exclude(project_code=None)
return queryset.filter(project_code=None)
assigned_to = rest_filters.ModelChoiceFilter(
queryset=Owner.objects.all(), field_name='responsible', label=_('Responsible')
)
created_by = rest_filters.ModelChoiceFilter(
queryset=User.objects.all(), field_name='created_by', label=_('Created By')
)
created_before = InvenTreeDateFilter(
label=_('Created Before'), field_name='creation_date', lookup_expr='lt'
)
created_after = InvenTreeDateFilter(
label=_('Created After'), field_name='creation_date', lookup_expr='gt'
)
has_start_date = rest_filters.BooleanFilter(
label=_('Has Start Date'), method='filter_has_start_date'
)
def filter_has_start_date(self, queryset, name, value):
"""Filter by whether or not the order has a start date."""
return queryset.filter(start_date__isnull=not str2bool(value))
start_date_before = InvenTreeDateFilter(
label=_('Start Date Before'), field_name='start_date', lookup_expr='lt'
)
start_date_after = InvenTreeDateFilter(
label=_('Start Date After'), field_name='start_date', lookup_expr='gt'
)
has_target_date = rest_filters.BooleanFilter(
label=_('Has Target Date'), method='filter_has_target_date'
)
def filter_has_target_date(self, queryset, name, value):
"""Filter by whether or not the order has a target date."""
return queryset.filter(target_date__isnull=not str2bool(value))
target_date_before = InvenTreeDateFilter(
label=_('Target Date Before'), field_name='target_date', lookup_expr='lt'
)
target_date_after = InvenTreeDateFilter(
label=_('Target Date After'), field_name='target_date', lookup_expr='gt'
)
min_date = InvenTreeDateFilter(label=_('Min Date'), method='filter_min_date')
def filter_min_date(self, queryset, name, value):
"""Filter the queryset to include orders *after* a specified date.
This is used in combination with filter_max_date,
to provide a queryset which matches a particular range of dates.
In particular, this is used in the UI for the calendar view.
"""
q1 = Q(
creation_date__gte=value, issue_date__isnull=True, start_date__isnull=True
)
q2 = Q(issue_date__gte=value, start_date__isnull=True)
q3 = Q(start_date__gte=value)
q4 = Q(target_date__gte=value)
return queryset.filter(q1 | q2 | q3 | q4).distinct()
max_date = InvenTreeDateFilter(label=_('Max Date'), method='filter_max_date')
def filter_max_date(self, queryset, name, value):
"""Filter the queryset to include orders *before* a specified date.
This is used in combination with filter_min_date,
to provide a queryset which matches a particular range of dates.
In particular, this is used in the UI for the calendar view.
"""
q1 = Q(
creation_date__lte=value, issue_date__isnull=True, start_date__isnull=True
)
q2 = Q(issue_date__lte=value, start_date__isnull=True)
q3 = Q(start_date__lte=value)
q4 = Q(target_date__lte=value)
return queryset.filter(q1 | q2 | q3 | q4).distinct()
class LineItemFilter(rest_filters.FilterSet):
"""Base class for custom API filters for order line item list(s)."""
# Filter by order status
order_status = rest_filters.NumberFilter(
label=_('Order Status'), field_name='order__status'
)
has_pricing = rest_filters.BooleanFilter(
label=_('Has Pricing'), method='filter_has_pricing'
)
def filter_has_pricing(self, queryset, name, value):
"""Filter by whether or not the line item has pricing information."""
filters = {self.Meta.price_field: None}
if str2bool(value):
return queryset.exclude(**filters)
return queryset.filter(**filters)
class PurchaseOrderFilter(OrderFilter):
"""Custom API filters for the PurchaseOrderList endpoint."""
class Meta:
"""Metaclass options."""
model = models.PurchaseOrder
fields = ['supplier']
part = rest_filters.ModelChoiceFilter(
queryset=Part.objects.all(),
field_name='part',
label=_('Part'),
method='filter_part',
)
def filter_part(self, queryset, name, part: Part):
"""Filter by provided Part instance."""
orders = part.purchase_orders()
return queryset.filter(pk__in=[o.pk for o in orders])
supplier_part = rest_filters.ModelChoiceFilter(
queryset=company.models.SupplierPart.objects.all(),
label=_('Supplier Part'),
method='filter_supplier_part',
)
def filter_supplier_part(
self, queryset, name, supplier_part: company.models.SupplierPart
):
"""Filter by provided SupplierPart instance."""
orders = supplier_part.purchase_orders()
return queryset.filter(pk__in=[o.pk for o in orders])
completed_before = InvenTreeDateFilter(
label=_('Completed Before'), field_name='complete_date', lookup_expr='lt'
)
completed_after = InvenTreeDateFilter(
label=_('Completed After'), field_name='complete_date', lookup_expr='gt'
)
class PurchaseOrderMixin:
"""Mixin class for PurchaseOrder endpoints."""
queryset = models.PurchaseOrder.objects.all()
serializer_class = serializers.PurchaseOrderSerializer
def get_serializer(self, *args, **kwargs):
"""Return the serializer instance for this endpoint."""
try:
kwargs['supplier_detail'] = str2bool(
self.request.query_params.get('supplier_detail', False)
)
except AttributeError:
pass
# Ensure the request context is passed through
kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs)
def get_queryset(self, *args, **kwargs):
"""Return the annotated queryset for this endpoint."""
queryset = super().get_queryset(*args, **kwargs)
queryset = queryset.prefetch_related(
'supplier', 'project_code', 'lines', 'responsible'
)
queryset = serializers.PurchaseOrderSerializer.annotate_queryset(queryset)
return queryset
class PurchaseOrderList(
PurchaseOrderMixin, OrderCreateMixin, DataExportViewMixin, ListCreateAPI
):
"""API endpoint for accessing a list of PurchaseOrder objects.
- GET: Return list of PurchaseOrder objects (with filters)
- POST: Create a new PurchaseOrder object
"""
filterset_class = PurchaseOrderFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS
ordering_field_aliases = {
'reference': ['reference_int', 'reference'],
'project_code': ['project_code__code'],
}
search_fields = [
'reference',
'supplier__name',
'supplier_reference',
'project_code__code',
'description',
]
ordering_fields = [
'creation_date',
'created_by',
'reference',
'supplier__name',
'start_date',
'target_date',
'complete_date',
'line_items',
'status',
'responsible',
'total_price',
'project_code',
]
ordering = '-reference'
class PurchaseOrderDetail(PurchaseOrderMixin, RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a PurchaseOrder object."""
class PurchaseOrderContextMixin:
"""Mixin to add purchase order object as serializer context variable."""
queryset = models.PurchaseOrder.objects.all()
def get_serializer_context(self):
"""Add the PurchaseOrder object to the serializer context."""
context = super().get_serializer_context()
# Pass the purchase order through to the serializer for validation
try:
context['order'] = models.PurchaseOrder.objects.get(
pk=self.kwargs.get('pk', None)
)
except Exception:
pass
context['request'] = self.request
return context
class PurchaseOrderHold(PurchaseOrderContextMixin, CreateAPI):
"""API endpoint to place a PurchaseOrder on hold."""
serializer_class = serializers.PurchaseOrderHoldSerializer
class PurchaseOrderCancel(PurchaseOrderContextMixin, CreateAPI):
"""API endpoint to 'cancel' a purchase order.
The purchase order must be in a state which can be cancelled
"""
serializer_class = serializers.PurchaseOrderCancelSerializer
class PurchaseOrderComplete(PurchaseOrderContextMixin, CreateAPI):
"""API endpoint to 'complete' a purchase order."""
serializer_class = serializers.PurchaseOrderCompleteSerializer
class PurchaseOrderIssue(PurchaseOrderContextMixin, CreateAPI):
"""API endpoint to 'issue' (place) a PurchaseOrder."""
serializer_class = serializers.PurchaseOrderIssueSerializer
class PurchaseOrderReceive(PurchaseOrderContextMixin, CreateAPI):
"""API endpoint to receive stock items against a PurchaseOrder.
- The purchase order is specified in the URL.
- Items to receive are specified as a list called "items" with the following options:
- line_item: pk of the PO Line item
- supplier_part: pk value of the supplier part
- quantity: quantity to receive
- status: stock item status
- expiry_date: stock item expiry date (optional)
- location: destination for stock item (optional)
- batch_code: the batch code for this stock item
- serial_numbers: serial numbers for this stock item
- A global location must also be specified. This is used when no locations are specified for items, and no location is given in the PO line item
"""
queryset = models.PurchaseOrderLineItem.objects.none()
serializer_class = serializers.PurchaseOrderReceiveSerializer
class PurchaseOrderLineItemFilter(LineItemFilter):
"""Custom filters for the PurchaseOrderLineItemList endpoint."""
class Meta:
"""Metaclass options."""
price_field = 'purchase_price'
model = models.PurchaseOrderLineItem
fields = []
order = rest_filters.ModelChoiceFilter(
queryset=models.PurchaseOrder.objects.all(),
field_name='order',
label=_('Order'),
)
order_complete = rest_filters.BooleanFilter(
label=_('Order Complete'), method='filter_order_complete'
)
def filter_order_complete(self, queryset, name, value):
"""Filter by whether the order is 'complete' or not."""
if str2bool(value):
return queryset.filter(order__status=PurchaseOrderStatus.COMPLETE.value)
return queryset.exclude(order__status=PurchaseOrderStatus.COMPLETE.value)
part = rest_filters.ModelChoiceFilter(
queryset=company.models.SupplierPart.objects.all(),
field_name='part',
label=_('Supplier Part'),
)
include_variants = rest_filters.BooleanFilter(
label=_('Include Variants'), method='filter_include_variants'
)
def filter_include_variants(self, queryset, name, value):
"""Filter by whether or not to include variants of the selected part.
Note:
- This filter does nothing by itself, and requires the 'base_part' filter to be set.
- Refer to the 'filter_base_part' method for more information.
"""
return queryset
base_part = rest_filters.ModelChoiceFilter(
queryset=Part.objects.filter(purchaseable=True),
method='filter_base_part',
label=_('Internal Part'),
)
def filter_base_part(self, queryset, name, base_part):
"""Filter by the 'base_part' attribute.
Note:
- If "include_variants" is True, include all variants of the selected part
- Otherwise, just filter by the selected part
"""
include_variants = str2bool(self.data.get('include_variants', False))
if include_variants:
parts = base_part.get_descendants(include_self=True)
return queryset.filter(part__part__in=parts)
else:
return queryset.filter(part__part=base_part)
pending = rest_filters.BooleanFilter(
method='filter_pending', label=_('Order Pending')
)
def filter_pending(self, queryset, name, value):
"""Filter by "pending" status (order status = pending)."""
if str2bool(value):
return queryset.filter(order__status__in=PurchaseOrderStatusGroups.OPEN)
return queryset.exclude(order__status__in=PurchaseOrderStatusGroups.OPEN)
received = rest_filters.BooleanFilter(
label=_('Items Received'), method='filter_received'
)
def filter_received(self, queryset, name, value):
"""Filter by lines which are "received" (or "not" received).
A line is considered "received" when received >= quantity
"""
q = Q(received__gte=F('quantity'))
if str2bool(value):
return queryset.filter(q)
# Only count "pending" orders
return queryset.exclude(q).filter(
order__status__in=PurchaseOrderStatusGroups.OPEN
)
class PurchaseOrderLineItemMixin:
"""Mixin class for PurchaseOrderLineItem endpoints."""
queryset = models.PurchaseOrderLineItem.objects.all()
serializer_class = serializers.PurchaseOrderLineItemSerializer
def get_queryset(self, *args, **kwargs):
"""Return annotated queryset for this endpoint."""
queryset = super().get_queryset(*args, **kwargs)
queryset = serializers.PurchaseOrderLineItemSerializer.annotate_queryset(
queryset
)
return queryset
def get_serializer(self, *args, **kwargs):
"""Return serializer instance for this endpoint."""
try:
kwargs['part_detail'] = str2bool(
self.request.query_params.get('part_detail', False)
)
kwargs['order_detail'] = str2bool(
self.request.query_params.get('order_detail', False)
)
except AttributeError:
pass
kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs)
def perform_update(self, serializer):
"""Override the perform_update method to auto-update pricing if required."""
super().perform_update(serializer)
# possibly auto-update pricing based on the supplier part pricing data
if serializer.validated_data.get('auto_pricing', True):
serializer.instance.update_pricing()
class PurchaseOrderLineItemList(
PurchaseOrderLineItemMixin, DataExportViewMixin, ListCreateDestroyAPIView
):
"""API endpoint for accessing a list of PurchaseOrderLineItem objects.
- GET: Return a list of PurchaseOrder Line Item objects
- POST: Create a new PurchaseOrderLineItem object
"""
filterset_class = PurchaseOrderLineItemFilter
def create(self, request, *args, **kwargs):
"""Create or update a new PurchaseOrderLineItem object."""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
data = cast(dict, serializer.validated_data)
# possibly merge duplicate items
line_item = None
if data.get('merge_items', True):
other_line = models.PurchaseOrderLineItem.objects.filter(
part=data.get('part'),
order=data.get('order'),
target_date=data.get('target_date'),
destination=data.get('destination'),
).first()
if other_line is not None:
other_line.quantity += Decimal(data.get('quantity', 0))
other_line.save()
line_item = other_line
# otherwise create a new line item
if line_item is None:
line_item = serializer.save()
# possibly auto-update pricing based on the supplier part pricing data
if data.get('auto_pricing', True) and isinstance(
line_item, models.PurchaseOrderLineItem
):
line_item.update_pricing()
serializer = serializers.PurchaseOrderLineItemSerializer(line_item)
headers = self.get_success_headers(serializer.data)
return Response(
serializer.data, status=status.HTTP_201_CREATED, headers=headers
)
filter_backends = SEARCH_ORDER_FILTER_ALIAS
ordering_field_aliases = {
'MPN': 'part__manufacturer_part__MPN',
'SKU': 'part__SKU',
'IPN': 'part__part__IPN',
'part_name': 'part__part__name',
'order': 'order__reference',
'status': 'order__status',
'complete_date': 'order__complete_date',
}
ordering_fields = [
'MPN',
'part_name',
'purchase_price',
'quantity',
'received',
'reference',
'SKU',
'IPN',
'total_price',
'target_date',
'order',
'status',
'complete_date',
]
search_fields = [
'part__part__name',
'part__part__description',
'part__manufacturer_part__MPN',
'part__SKU',
'reference',
]
class PurchaseOrderLineItemDetail(PurchaseOrderLineItemMixin, RetrieveUpdateDestroyAPI):
"""Detail API endpoint for PurchaseOrderLineItem object."""
class PurchaseOrderExtraLineList(GeneralExtraLineList, ListCreateAPI):
"""API endpoint for accessing a list of PurchaseOrderExtraLine objects."""
queryset = models.PurchaseOrderExtraLine.objects.all()
serializer_class = serializers.PurchaseOrderExtraLineSerializer
class PurchaseOrderExtraLineDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a PurchaseOrderExtraLine object."""
queryset = models.PurchaseOrderExtraLine.objects.all()
serializer_class = serializers.PurchaseOrderExtraLineSerializer
class SalesOrderFilter(OrderFilter):
"""Custom API filters for the SalesOrderList endpoint."""
class Meta:
"""Metaclass options."""
model = models.SalesOrder
fields = ['customer']
include_variants = rest_filters.BooleanFilter(
label=_('Include Variants'), method='filter_include_variants'
)
def filter_include_variants(self, queryset, name, value):
"""Filter by whether or not to include variants of the selected part.
Note:
- This filter does nothing by itself, and requires the 'part' filter to be set.
- Refer to the 'filter_part' method for more information.
"""
return queryset
part = rest_filters.ModelChoiceFilter(
queryset=Part.objects.all(), field_name='part', method='filter_part'
)
def filter_part(self, queryset, name, part):
"""Filter SalesOrder by selected 'part'.
Note:
- If 'include_variants' is set to True, then all variants of the selected part will be included.
- Otherwise, just filter by the selected part.
"""
include_variants = str2bool(self.data.get('include_variants', False))
# Construct a queryset of parts to filter by
if include_variants:
parts = part.get_descendants(include_self=True)
else:
parts = Part.objects.filter(pk=part.pk)
# Now that we have a queryset of parts, find all the matching sales orders
line_items = models.SalesOrderLineItem.objects.filter(part__in=parts)
# Generate a list of ID values for the matching sales orders
sales_orders = line_items.values_list('order', flat=True).distinct()
# Now we have a list of matching IDs, filter the queryset
return queryset.filter(pk__in=sales_orders)
completed_before = InvenTreeDateFilter(
label=_('Completed Before'), field_name='shipment_date', lookup_expr='lt'
)
completed_after = InvenTreeDateFilter(
label=_('Completed After'), field_name='shipment_date', lookup_expr='gt'
)
class SalesOrderMixin:
"""Mixin class for SalesOrder endpoints."""
queryset = models.SalesOrder.objects.all()
serializer_class = serializers.SalesOrderSerializer
def get_serializer(self, *args, **kwargs):
"""Return serializer instance for this endpoint."""
try:
kwargs['customer_detail'] = str2bool(
self.request.query_params.get('customer_detail', False)
)
except AttributeError:
pass
# Ensure the context is passed through to the serializer
kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs)
def get_queryset(self, *args, **kwargs):
"""Return annotated queryset for this endpoint."""
queryset = super().get_queryset(*args, **kwargs)
queryset = queryset.prefetch_related(
'customer', 'responsible', 'project_code', 'lines'
)
queryset = serializers.SalesOrderSerializer.annotate_queryset(queryset)
return queryset
class SalesOrderList(
SalesOrderMixin, OrderCreateMixin, DataExportViewMixin, ListCreateAPI
):
"""API endpoint for accessing a list of SalesOrder objects.
- GET: Return list of SalesOrder objects (with filters)
- POST: Create a new SalesOrder
"""
filterset_class = SalesOrderFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS
ordering_field_aliases = {
'reference': ['reference_int', 'reference'],
'project_code': ['project_code__code'],
}
filterset_fields = ['customer']
ordering_fields = [
'creation_date',
'created_by',
'reference',
'customer__name',
'customer_reference',
'status',
'start_date',
'target_date',
'line_items',
'shipment_date',
'total_price',
'project_code',
]
search_fields = [
'customer__name',
'reference',
'description',
'customer_reference',
'project_code__code',
]
ordering = '-reference'
class SalesOrderDetail(SalesOrderMixin, RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a SalesOrder object."""
class SalesOrderLineItemFilter(LineItemFilter):
"""Custom filters for SalesOrderLineItemList endpoint."""
class Meta:
"""Metaclass options."""
price_field = 'sale_price'
model = models.SalesOrderLineItem
fields = []
order = rest_filters.ModelChoiceFilter(
queryset=models.SalesOrder.objects.all(), field_name='order', label=_('Order')
)
part = rest_filters.ModelChoiceFilter(
queryset=Part.objects.all(), field_name='part', label=_('Part')
)
allocated = rest_filters.BooleanFilter(
label=_('Allocated'), method='filter_allocated'
)
def filter_allocated(self, queryset, name, value):
"""Filter by lines which are 'allocated'.
A line is 'allocated' when allocated >= quantity
"""
q = Q(allocated__gte=F('quantity'))
if str2bool(value):
return queryset.filter(q)
return queryset.exclude(q)
completed = rest_filters.BooleanFilter(
label=_('Completed'), method='filter_completed'
)
def filter_completed(self, queryset, name, value):
"""Filter by lines which are "completed".
A line is 'completed' when shipped >= quantity
"""
q = Q(shipped__gte=F('quantity'))
if str2bool(value):
return queryset.filter(q)
return queryset.exclude(q)
order_complete = rest_filters.BooleanFilter(
label=_('Order Complete'), method='filter_order_complete'
)
def filter_order_complete(self, queryset, name, value):
"""Filter by whether the order is 'complete' or not."""
if str2bool(value):
return queryset.filter(order__status__in=SalesOrderStatusGroups.COMPLETE)
return queryset.exclude(order__status__in=SalesOrderStatusGroups.COMPLETE)
order_outstanding = rest_filters.BooleanFilter(
label=_('Order Outstanding'), method='filter_order_outstanding'
)
def filter_order_outstanding(self, queryset, name, value):
"""Filter by whether the order is 'outstanding' or not."""
if str2bool(value):
return queryset.filter(order__status__in=SalesOrderStatusGroups.OPEN)
return queryset.exclude(order__status__in=SalesOrderStatusGroups.OPEN)
class SalesOrderLineItemMixin:
"""Mixin class for SalesOrderLineItem endpoints."""
queryset = models.SalesOrderLineItem.objects.all()
serializer_class = serializers.SalesOrderLineItemSerializer
def get_serializer(self, *args, **kwargs):
"""Return serializer for this endpoint with extra data as requested."""
try:
params = self.request.query_params
kwargs['part_detail'] = str2bool(params.get('part_detail', False))
kwargs['order_detail'] = str2bool(params.get('order_detail', False))
kwargs['customer_detail'] = str2bool(params.get('customer_detail', False))
except AttributeError:
pass
kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs)
def get_queryset(self, *args, **kwargs):
"""Return annotated queryset for this endpoint."""
queryset = super().get_queryset(*args, **kwargs)
queryset = queryset.prefetch_related(
'part',
'part__stock_items',
'allocations',
'allocations__shipment',
'allocations__item__part',
'allocations__item__location',
'order',
'order__stock_items',
)
queryset = queryset.select_related('part__pricing_data')
queryset = serializers.SalesOrderLineItemSerializer.annotate_queryset(queryset)
return queryset
class SalesOrderLineItemList(
SalesOrderLineItemMixin, DataExportViewMixin, ListCreateAPI
):
"""API endpoint for accessing a list of SalesOrderLineItem objects."""
filterset_class = SalesOrderLineItemFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS
ordering_fields = [
'customer',
'order',
'part',
'part__name',
'quantity',
'allocated',
'shipped',
'reference',
'sale_price',
'target_date',
]
ordering_field_aliases = {
'customer': 'order__customer__name',
'part': 'part__name',
'order': 'order__reference',
}
search_fields = ['part__name', 'quantity', 'reference']
class SalesOrderLineItemDetail(SalesOrderLineItemMixin, RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a SalesOrderLineItem object."""
class SalesOrderExtraLineList(GeneralExtraLineList, ListCreateAPI):
"""API endpoint for accessing a list of SalesOrderExtraLine objects."""
queryset = models.SalesOrderExtraLine.objects.all()
serializer_class = serializers.SalesOrderExtraLineSerializer
class SalesOrderExtraLineDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a SalesOrderExtraLine object."""
queryset = models.SalesOrderExtraLine.objects.all()
serializer_class = serializers.SalesOrderExtraLineSerializer
class SalesOrderContextMixin:
"""Mixin to add sales order object as serializer context variable."""
queryset = models.SalesOrder.objects.all()
def get_serializer_context(self):
"""Add the 'order' reference to the serializer context for any classes which inherit this mixin."""
ctx = super().get_serializer_context()
ctx['request'] = self.request
try:
ctx['order'] = models.SalesOrder.objects.get(pk=self.kwargs.get('pk', None))
except Exception:
pass
return ctx
class SalesOrderHold(SalesOrderContextMixin, CreateAPI):
"""API endpoint to place a SalesOrder on hold."""
serializer_class = serializers.SalesOrderHoldSerializer
class SalesOrderCancel(SalesOrderContextMixin, CreateAPI):
"""API endpoint to cancel a SalesOrder."""
serializer_class = serializers.SalesOrderCancelSerializer
class SalesOrderIssue(SalesOrderContextMixin, CreateAPI):
"""API endpoint to issue a SalesOrder."""
serializer_class = serializers.SalesOrderIssueSerializer
class SalesOrderComplete(SalesOrderContextMixin, CreateAPI):
"""API endpoint for manually marking a SalesOrder as "complete"."""
serializer_class = serializers.SalesOrderCompleteSerializer
class SalesOrderAllocateSerials(SalesOrderContextMixin, CreateAPI):
"""API endpoint to allocation stock items against a SalesOrder, by specifying serial numbers."""
queryset = models.SalesOrder.objects.none()
serializer_class = serializers.SalesOrderSerialAllocationSerializer
class SalesOrderAllocate(SalesOrderContextMixin, CreateAPI):
"""API endpoint to allocate stock items against a SalesOrder.
- The SalesOrder is specified in the URL
- See the SalesOrderShipmentAllocationSerializer class
"""
queryset = models.SalesOrder.objects.none()
serializer_class = serializers.SalesOrderShipmentAllocationSerializer
class SalesOrderAllocationFilter(rest_filters.FilterSet):
"""Custom filterset for the SalesOrderAllocationList endpoint."""
class Meta:
"""Metaclass options."""
model = models.SalesOrderAllocation
fields = ['shipment', 'line', 'item']
order = rest_filters.ModelChoiceFilter(
queryset=models.SalesOrder.objects.all(),
field_name='line__order',
label=_('Order'),
)
include_variants = rest_filters.BooleanFilter(
label=_('Include Variants'), method='filter_include_variants'
)
def filter_include_variants(self, queryset, name, value):
"""Filter by whether or not to include variants of the selected part.
Note:
- This filter does nothing by itself, and requires the 'part' filter to be set.
- Refer to the 'filter_part' method for more information.
"""
return queryset
part = rest_filters.ModelChoiceFilter(
queryset=Part.objects.all(), method='filter_part', label=_('Part')
)
def filter_part(self, queryset, name, part):
"""Filter by the 'part' attribute.
Note:
- If "include_variants" is True, include all variants of the selected part
- Otherwise, just filter by the selected part
"""
include_variants = str2bool(self.data.get('include_variants', False))
if include_variants:
parts = part.get_descendants(include_self=True)
return queryset.filter(item__part__in=parts)
else:
return queryset.filter(item__part=part)
outstanding = rest_filters.BooleanFilter(
label=_('Outstanding'), method='filter_outstanding'
)
def filter_outstanding(self, queryset, name, value):
"""Filter by "outstanding" status (boolean)."""
if str2bool(value):
return queryset.filter(
line__order__status__in=SalesOrderStatusGroups.OPEN,
shipment__shipment_date=None,
)
return queryset.exclude(
shipment__shipment_date=None,
line__order__status__in=SalesOrderStatusGroups.OPEN,
)
assigned_to_shipment = rest_filters.BooleanFilter(
label=_('Has Shipment'), method='filter_assigned_to_shipment'
)
def filter_assigned_to_shipment(self, queryset, name, value):
"""Filter by whether or not the allocation has been assigned to a shipment."""
if str2bool(value):
return queryset.exclude(shipment=None)
return queryset.filter(shipment=None)
class SalesOrderAllocationMixin:
"""Mixin class for SalesOrderAllocation endpoints."""
queryset = models.SalesOrderAllocation.objects.all()
serializer_class = serializers.SalesOrderAllocationSerializer
def get_queryset(self, *args, **kwargs):
"""Annotate the queryset for this endpoint."""
queryset = super().get_queryset(*args, **kwargs)
queryset = queryset.prefetch_related(
'item',
'item__sales_order',
'item__part',
'line__part',
'item__location',
'line__order',
'line__order__responsible',
'line__order__project_code',
'line__order__project_code__responsible',
'shipment',
'shipment__order',
'shipment__checked_by',
).select_related('line__part__pricing_data', 'item__part__pricing_data')
return queryset
class SalesOrderAllocationList(SalesOrderAllocationMixin, ListAPI):
"""API endpoint for listing SalesOrderAllocation objects."""
filterset_class = SalesOrderAllocationFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS
ordering_fields = [
'quantity',
'part',
'serial',
'batch',
'location',
'order',
'shipment_date',
]
ordering_field_aliases = {
'part': 'item__part__name',
'serial': ['item__serial_int', 'item__serial'],
'batch': 'item__batch',
'location': 'item__location__name',
'order': 'line__order__reference',
'shipment_date': 'shipment__shipment_date',
}
search_fields = {'item__part__name', 'item__serial', 'item__batch'}
def get_serializer(self, *args, **kwargs):
"""Return the serializer instance for this endpoint.
Adds extra detail serializers if requested
"""
try:
params = self.request.query_params
kwargs['part_detail'] = str2bool(params.get('part_detail', False))
kwargs['item_detail'] = str2bool(params.get('item_detail', False))
kwargs['order_detail'] = str2bool(params.get('order_detail', False))
kwargs['location_detail'] = str2bool(params.get('location_detail', False))
kwargs['customer_detail'] = str2bool(params.get('customer_detail', False))
except AttributeError:
pass
return self.serializer_class(*args, **kwargs)
class SalesOrderAllocationDetail(SalesOrderAllocationMixin, RetrieveUpdateDestroyAPI):
"""API endpoint for detali view of a SalesOrderAllocation object."""
class SalesOrderShipmentFilter(rest_filters.FilterSet):
"""Custom filterset for the SalesOrderShipmentList endpoint."""
class Meta:
"""Metaclass options."""
model = models.SalesOrderShipment
fields = ['order']
shipped = rest_filters.BooleanFilter(label='shipped', method='filter_shipped')
def filter_shipped(self, queryset, name, value):
"""Filter SalesOrder list by 'shipped' status (boolean)."""
if str2bool(value):
return queryset.exclude(shipment_date=None)
return queryset.filter(shipment_date=None)
delivered = rest_filters.BooleanFilter(label='delivered', method='filter_delivered')
def filter_delivered(self, queryset, name, value):
"""Filter SalesOrder list by 'delivered' status (boolean)."""
if str2bool(value):
return queryset.exclude(delivery_date=None)
return queryset.filter(delivery_date=None)
class SalesOrderShipmentMixin:
"""Mixin class for SalesOrderShipment endpoints."""
queryset = models.SalesOrderShipment.objects.all()
serializer_class = serializers.SalesOrderShipmentSerializer
def get_queryset(self, *args, **kwargs):
"""Return annotated queryset for this endpoint."""
queryset = super().get_queryset(*args, **kwargs)
queryset = serializers.SalesOrderShipmentSerializer.annotate_queryset(queryset)
return queryset
class SalesOrderShipmentList(SalesOrderShipmentMixin, ListCreateAPI):
"""API list endpoint for SalesOrderShipment model."""
filterset_class = SalesOrderShipmentFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS
ordering_fields = ['reference', 'delivery_date', 'shipment_date', 'allocated_items']
search_fields = [
'order__reference',
'reference',
'tracking_number',
'invoice_number',
]
class SalesOrderShipmentDetail(SalesOrderShipmentMixin, RetrieveUpdateDestroyAPI):
"""API detail endpooint for SalesOrderShipment model."""
class SalesOrderShipmentComplete(CreateAPI):
"""API endpoint for completing (shipping) a SalesOrderShipment."""
queryset = models.SalesOrderShipment.objects.all()
serializer_class = serializers.SalesOrderShipmentCompleteSerializer
def get_serializer_context(self):
"""Pass the request object to the serializer."""
ctx = super().get_serializer_context()
ctx['request'] = self.request
try:
ctx['shipment'] = models.SalesOrderShipment.objects.get(
pk=self.kwargs.get('pk', None)
)
except Exception:
pass
return ctx
class ReturnOrderFilter(OrderFilter):
"""Custom API filters for the ReturnOrderList endpoint."""
class Meta:
"""Metaclass options."""
model = models.ReturnOrder
fields = ['customer']
include_variants = rest_filters.BooleanFilter(
label=_('Include Variants'), method='filter_include_variants'
)
def filter_include_variants(self, queryset, name, value):
"""Filter by whether or not to include variants of the selected part.
Note:
- This filter does nothing by itself, and requires the 'part' filter to be set.
- Refer to the 'filter_part' method for more information.
"""
return queryset
part = rest_filters.ModelChoiceFilter(
queryset=Part.objects.all(), field_name='part', method='filter_part'
)
def filter_part(self, queryset, name, part):
"""Filter by selected 'part'.
Note:
- If 'include_variants' is set to True, then all variants of the selected part will be included.
- Otherwise, just filter by the selected part.
"""
include_variants = str2bool(self.data.get('include_variants', False))
if include_variants:
parts = part.get_descendants(include_self=True)
else:
parts = Part.objects.filter(pk=part.pk)
# Now that we have a queryset of parts, find all the matching return orders
line_items = models.ReturnOrderLineItem.objects.filter(item__part__in=parts)
# Generate a list of ID values for the matching return orders
return_orders = line_items.values_list('order', flat=True).distinct()
# Now we have a list of matching IDs, filter the queryset
return queryset.filter(pk__in=return_orders)
completed_before = InvenTreeDateFilter(
label=_('Completed Before'), field_name='complete_date', lookup_expr='lt'
)
completed_after = InvenTreeDateFilter(
label=_('Completed After'), field_name='complete_date', lookup_expr='gt'
)
class ReturnOrderMixin:
"""Mixin class for ReturnOrder endpoints."""
queryset = models.ReturnOrder.objects.all()
serializer_class = serializers.ReturnOrderSerializer
def get_serializer(self, *args, **kwargs):
"""Return serializer instance for this endpoint."""
try:
kwargs['customer_detail'] = str2bool(
self.request.query_params.get('customer_detail', False)
)
except AttributeError:
pass
# Ensure the context is passed through to the serializer
kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs)
def get_queryset(self, *args, **kwargs):
"""Return annotated queryset for this endpoint."""
queryset = super().get_queryset(*args, **kwargs)
queryset = queryset.prefetch_related(
'customer', 'lines', 'project_code', 'responsible'
)
queryset = serializers.ReturnOrderSerializer.annotate_queryset(queryset)
return queryset
class ReturnOrderList(
ReturnOrderMixin, OrderCreateMixin, DataExportViewMixin, ListCreateAPI
):
"""API endpoint for accessing a list of ReturnOrder objects."""
filterset_class = ReturnOrderFilter
filter_backends = SEARCH_ORDER_FILTER_ALIAS
ordering_field_aliases = {
'reference': ['reference_int', 'reference'],
'project_code': ['project_code__code'],
}
ordering_fields = [
'creation_date',
'created_by',
'reference',
'customer__name',
'customer_reference',
'line_items',
'status',
'start_date',
'target_date',
'complete_date',
'project_code',
]
search_fields = [
'customer__name',
'reference',
'description',
'customer_reference',
'project_code__code',
]
ordering = '-reference'
class ReturnOrderDetail(ReturnOrderMixin, RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a single ReturnOrder object."""
class ReturnOrderContextMixin:
"""Simple mixin class to add a ReturnOrder to the serializer context."""
queryset = models.ReturnOrder.objects.all()
def get_serializer_context(self):
"""Add the PurchaseOrder object to the serializer context."""
context = super().get_serializer_context()
# Pass the ReturnOrder instance through to the serializer for validation
try:
context['order'] = models.ReturnOrder.objects.get(
pk=self.kwargs.get('pk', None)
)
except Exception:
pass
context['request'] = self.request
return context
class ReturnOrderCancel(ReturnOrderContextMixin, CreateAPI):
"""API endpoint to cancel a ReturnOrder."""
serializer_class = serializers.ReturnOrderCancelSerializer
class ReturnOrderHold(ReturnOrderContextMixin, CreateAPI):
"""API endpoint to hold a ReturnOrder."""
serializer_class = serializers.ReturnOrderHoldSerializer
class ReturnOrderComplete(ReturnOrderContextMixin, CreateAPI):
"""API endpoint to complete a ReturnOrder."""
serializer_class = serializers.ReturnOrderCompleteSerializer
class ReturnOrderIssue(ReturnOrderContextMixin, CreateAPI):
"""API endpoint to issue (place) a ReturnOrder."""
serializer_class = serializers.ReturnOrderIssueSerializer
class ReturnOrderReceive(ReturnOrderContextMixin, CreateAPI):
"""API endpoint to receive items against a ReturnOrder."""
queryset = models.ReturnOrder.objects.none()
serializer_class = serializers.ReturnOrderReceiveSerializer
class ReturnOrderLineItemFilter(LineItemFilter):
"""Custom filters for the ReturnOrderLineItemList endpoint."""
class Meta:
"""Metaclass options."""
price_field = 'price'
model = models.ReturnOrderLineItem
fields = ['order', 'item']
outcome = rest_filters.NumberFilter(label='outcome')
received = rest_filters.BooleanFilter(label='received', method='filter_received')
def filter_received(self, queryset, name, value):
"""Filter by 'received' field."""
if str2bool(value):
return queryset.exclude(received_date=None)
return queryset.filter(received_date=None)
class ReturnOrderLineItemMixin:
"""Mixin class for ReturnOrderLineItem endpoints."""
queryset = models.ReturnOrderLineItem.objects.all()
serializer_class = serializers.ReturnOrderLineItemSerializer
def get_serializer(self, *args, **kwargs):
"""Return serializer for this endpoint with extra data as requested."""
try:
params = self.request.query_params
kwargs['order_detail'] = str2bool(params.get('order_detail', False))
kwargs['item_detail'] = str2bool(params.get('item_detail', True))
kwargs['part_detail'] = str2bool(params.get('part_detail', False))
except AttributeError:
pass
kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs)
def get_queryset(self, *args, **kwargs):
"""Return annotated queryset for this endpoint."""
queryset = super().get_queryset(*args, **kwargs)
queryset = queryset.prefetch_related('order', 'item', 'item__part')
return queryset
class ReturnOrderLineItemList(
ReturnOrderLineItemMixin, DataExportViewMixin, ListCreateAPI
):
"""API endpoint for accessing a list of ReturnOrderLineItemList objects."""
filterset_class = ReturnOrderLineItemFilter
filter_backends = SEARCH_ORDER_FILTER
ordering_fields = ['reference', 'target_date', 'received_date']
search_fields = [
'item_serial',
'item__part__name',
'item__part__description',
'reference',
]
class ReturnOrderLineItemDetail(ReturnOrderLineItemMixin, RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a ReturnOrderLineItem object."""
class ReturnOrderExtraLineList(GeneralExtraLineList, ListCreateAPI):
"""API endpoint for accessing a list of ReturnOrderExtraLine objects."""
queryset = models.ReturnOrderExtraLine.objects.all()
serializer_class = serializers.ReturnOrderExtraLineSerializer
class ReturnOrderExtraLineDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a ReturnOrderExtraLine object."""
queryset = models.ReturnOrderExtraLine.objects.all()
serializer_class = serializers.ReturnOrderExtraLineSerializer
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
"""
instance_url = get_base_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 'authorization' in request.headers:
auth = request.headers['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 and 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 = {}
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')
elif obj['ordertype'] == 'return-order':
ordertype_title = _('Return Order')
else:
ordertype_title = _('Unknown')
company_name = common.settings.get_global_setting('INVENTREE_COMPANY_NAME')
return f'{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 Orders.
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.value)
else:
outlist = models.PurchaseOrder.objects.filter(target_date__isnull=False)
elif obj['ordertype'] == 'sales-order':
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.value)
else:
outlist = models.SalesOrder.objects.filter(target_date__isnull=False)
elif obj['ordertype'] == 'return-order':
if obj['include_completed'] is False:
# Do not include completed orders from list in this case
# Complete status = 30
outlist = models.ReturnOrder.objects.filter(
target_date__isnull=False
).filter(status__lt=ReturnOrderStatus.COMPLETE.value)
else:
outlist = models.ReturnOrder.objects.filter(target_date__isnull=False)
else:
outlist = []
return outlist
def item_title(self, item):
"""Set the event title to the order reference."""
return f'{item.reference}'
def item_description(self, item):
"""Set the event description."""
return f'Company: {item.company.name}\nStatus: {item.get_status_display()}\nDescription: {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."""
return construct_absolute_url(item.get_absolute_url())
order_api_urls = [
# API endpoints for purchase orders
path(
'po/',
include([
# Individual purchase order detail URLs
path(
'<int:pk>/',
include([
path(
'cancel/', PurchaseOrderCancel.as_view(), name='api-po-cancel'
),
path('hold/', PurchaseOrderHold.as_view(), name='api-po-hold'),
path(
'complete/',
PurchaseOrderComplete.as_view(),
name='api-po-complete',
),
path('issue/', PurchaseOrderIssue.as_view(), name='api-po-issue'),
path(
'metadata/',
MetadataView.as_view(),
{'model': models.PurchaseOrder},
name='api-po-metadata',
),
path(
'receive/',
PurchaseOrderReceive.as_view(),
name='api-po-receive',
),
# PurchaseOrder detail API endpoint
path('', PurchaseOrderDetail.as_view(), name='api-po-detail'),
]),
),
# Purchase order status code information
path(
'status/',
StatusView.as_view(),
{StatusView.MODEL_REF: PurchaseOrderStatus},
name='api-po-status-codes',
),
# Purchase order list
path('', PurchaseOrderList.as_view(), name='api-po-list'),
]),
),
# API endpoints for purchase order line items
path(
'po-line/',
include([
path(
'<int:pk>/',
include([
path(
'metadata/',
MetadataView.as_view(),
{'model': models.PurchaseOrderLineItem},
name='api-po-line-metadata',
),
path(
'',
PurchaseOrderLineItemDetail.as_view(),
name='api-po-line-detail',
),
]),
),
path('', PurchaseOrderLineItemList.as_view(), name='api-po-line-list'),
]),
),
# API endpoints for purchase order extra line
path(
'po-extra-line/',
include([
path(
'<int:pk>/',
include([
path(
'metadata/',
MetadataView.as_view(),
{'model': models.PurchaseOrderExtraLine},
name='api-po-extra-line-metadata',
),
path(
'',
PurchaseOrderExtraLineDetail.as_view(),
name='api-po-extra-line-detail',
),
]),
),
path(
'', PurchaseOrderExtraLineList.as_view(), name='api-po-extra-line-list'
),
]),
),
# API endpoints for sales ordesr
path(
'so/',
include([
path(
'shipment/',
include([
path(
'<int:pk>/',
include([
path(
'ship/',
SalesOrderShipmentComplete.as_view(),
name='api-so-shipment-ship',
),
path(
'metadata/',
MetadataView.as_view(),
{'model': models.SalesOrderShipment},
name='api-so-shipment-metadata',
),
path(
'',
SalesOrderShipmentDetail.as_view(),
name='api-so-shipment-detail',
),
]),
),
path(
'',
SalesOrderShipmentList.as_view(),
name='api-so-shipment-list',
),
]),
),
# Sales order detail view
path(
'<int:pk>/',
include([
path(
'allocate/',
SalesOrderAllocate.as_view(),
name='api-so-allocate',
),
path(
'allocate-serials/',
SalesOrderAllocateSerials.as_view(),
name='api-so-allocate-serials',
),
path('hold/', SalesOrderHold.as_view(), name='api-so-hold'),
path('cancel/', SalesOrderCancel.as_view(), name='api-so-cancel'),
path('issue/', SalesOrderIssue.as_view(), name='api-so-issue'),
path(
'complete/',
SalesOrderComplete.as_view(),
name='api-so-complete',
),
path(
'metadata/',
MetadataView.as_view(),
{'model': models.SalesOrder},
name='api-so-metadata',
),
# SalesOrder detail endpoint
path('', SalesOrderDetail.as_view(), name='api-so-detail'),
]),
),
# Sales order status code information
path(
'status/',
StatusView.as_view(),
{StatusView.MODEL_REF: SalesOrderStatus},
name='api-so-status-codes',
),
# Sales order list view
path('', SalesOrderList.as_view(), name='api-so-list'),
]),
),
# API endpoints for sales order line items
path(
'so-line/',
include([
path(
'<int:pk>/',
include([
path(
'metadata/',
MetadataView.as_view(),
{'model': models.SalesOrderLineItem},
name='api-so-line-metadata',
),
path(
'',
SalesOrderLineItemDetail.as_view(),
name='api-so-line-detail',
),
]),
),
path('', SalesOrderLineItemList.as_view(), name='api-so-line-list'),
]),
),
# API endpoints for sales order extra line
path(
'so-extra-line/',
include([
path(
'<int:pk>/',
include([
path(
'metadata/',
MetadataView.as_view(),
{'model': models.SalesOrderExtraLine},
name='api-so-extra-line-metadata',
),
path(
'',
SalesOrderExtraLineDetail.as_view(),
name='api-so-extra-line-detail',
),
]),
),
path('', SalesOrderExtraLineList.as_view(), name='api-so-extra-line-list'),
]),
),
# API endpoints for sales order allocations
path(
'so-allocation/',
include([
path(
'<int:pk>/',
SalesOrderAllocationDetail.as_view(),
name='api-so-allocation-detail',
),
path('', SalesOrderAllocationList.as_view(), name='api-so-allocation-list'),
]),
),
# API endpoints for return orders
path(
'ro/',
include([
# Return Order detail endpoints
path(
'<int:pk>/',
include([
path(
'cancel/',
ReturnOrderCancel.as_view(),
name='api-return-order-cancel',
),
path('hold/', ReturnOrderHold.as_view(), name='api-ro-hold'),
path(
'complete/',
ReturnOrderComplete.as_view(),
name='api-return-order-complete',
),
path(
'issue/',
ReturnOrderIssue.as_view(),
name='api-return-order-issue',
),
path(
'receive/',
ReturnOrderReceive.as_view(),
name='api-return-order-receive',
),
path(
'metadata/',
MetadataView.as_view(),
{'model': models.ReturnOrder},
name='api-return-order-metadata',
),
path(
'', ReturnOrderDetail.as_view(), name='api-return-order-detail'
),
]),
),
# Return order status code information
path(
'status/',
StatusView.as_view(),
{StatusView.MODEL_REF: ReturnOrderStatus},
name='api-return-order-status-codes',
),
# Return Order list
path('', ReturnOrderList.as_view(), name='api-return-order-list'),
]),
),
# API endpoints for return order lines
path(
'ro-line/',
include([
path(
'<int:pk>/',
include([
path(
'metadata/',
MetadataView.as_view(),
{'model': models.ReturnOrderLineItem},
name='api-return-order-line-metadata',
),
path(
'',
ReturnOrderLineItemDetail.as_view(),
name='api-return-order-line-detail',
),
]),
),
# Return order line item status code information
path(
'status/',
StatusView.as_view(),
{StatusView.MODEL_REF: ReturnOrderLineStatus},
name='api-return-order-line-status-codes',
),
path(
'', ReturnOrderLineItemList.as_view(), name='api-return-order-line-list'
),
]),
),
# API endpoints for return order extra line
path(
'ro-extra-line/',
include([
path(
'<int:pk>/',
include([
path(
'metadata/',
MetadataView.as_view(),
{'model': models.ReturnOrderExtraLine},
name='api-return-order-extra-line-metadata',
),
path(
'',
ReturnOrderExtraLineDetail.as_view(),
name='api-return-order-extra-line-detail',
),
]),
),
path(
'',
ReturnOrderExtraLineList.as_view(),
name='api-return-order-extra-line-list',
),
]),
),
# API endpoint for subscribing to ICS calendar of purchase/sales/return orders
re_path(
r'^calendar/(?P<ordertype>purchase-order|sales-order|return-order)/calendar.ics',
OrderCalendarExport(),
name='api-po-so-calendar',
),
]