diff --git a/InvenTree/order/admin.py b/InvenTree/order/admin.py index 9e53ead630..de326881a9 100644 --- a/InvenTree/order/admin.py +++ b/InvenTree/order/admin.py @@ -277,6 +277,19 @@ class SalesOrderAllocationAdmin(ImportExportModelAdmin): autocomplete_fields = ('line', 'shipment', 'item',) +class ReturnOrderResource(InvenTreeResource): + """Class for managing import / export of ReturnOrder data""" + + class Meta: + """Metaclass options""" + model = models.ReturnOrder + skip_unchanged = True + clean_model_instances = True + exclude = [ + 'metadata', + ] + + class ReturnOrderAdmin(ImportExportModelAdmin): """Admin class for the ReturnOrder model""" diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index b4a77fa34d..67033b31ee 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -28,7 +28,7 @@ from InvenTree.mixins import (CreateAPI, ListAPI, ListCreateAPI, from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus from order.admin import (PurchaseOrderExtraLineResource, PurchaseOrderLineItemResource, PurchaseOrderResource, - SalesOrderExtraLineResource, + ReturnOrderResource, SalesOrderExtraLineResource, SalesOrderLineItemResource, SalesOrderResource) from part.models import Part from plugin.serializers import MetadataSerializer @@ -1213,6 +1213,106 @@ class PurchaseOrderAttachmentDetail(AttachmentMixin, RetrieveUpdateDestroyAPI): serializer_class = serializers.PurchaseOrderAttachmentSerializer +class ReturnOrderFilter(OrderFilter): + """Custom API filters for the ReturnOrderList endpoint""" + + class Meta: + """Metaclass options""" + + model = models.ReturnOrder + fields = [ + 'customer', + ] + + +class ReturnOrderList(APIDownloadMixin, ListCreateAPI): + """API endpoint for accessing a list of ReturnOrder objects""" + + queryset = models.ReturnOrder.objects.all() + serializer_class = serializers.ReturnOrderSerializer + filterset_class = ReturnOrderFilter + + 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 download_queryset(self, queryset, export_format): + """Download this queryset as a file""" + + dataset = ReturnOrderResource().export(queryset=queryset) + filedata = dataset.export(export_format) + filename = f"InvenTree_ReturnOrders.{export_format}" + + return DownloadFile(filedata, filename) + + def filter_queryset(self, queryset): + """Custom queryset filtering not supported by the ReturnOrderFilter class""" + + return queryset + + filter_backends = [ + rest_filters.DjangoFilterBackend, + filters.SearchFilter, + InvenTreeOrderingFilter, + ] + + ordering_field_aliases = { + 'reference': ['reference_int', 'reference'], + } + + filterset_fields = [ + 'customer', + ] + + ordering_fields = [ + 'creation_date', + 'reference', + 'customer__name', + 'customer_reference', + 'status', + 'target_date', + ] + + search_fields = [ + 'customer__name', + 'reference', + 'description', + 'customer_reference', + ] + + ordering = '-reference' + + +class ReturnOrderDetail(RetrieveUpdateDestroyAPI): + """API endpoint for detail view of a single ReturnOrder object""" + + queryset = models.ReturnOrder.objects.all() + serializer_class = serializers.ReturnOrderSerializer + + def get_serializer(self, *args, **kwargs): + """Return the serializer instance for this endpoint""" + try: + kwargs['customer_detail'] = str2bool( + self.request.query_params.get('customer_detail', False) + ) + except AttributeError: + pass + + kwargs['context'] = self.get_serializer_context() + + return self.serializer_class(*args, **kwargs) + + class OrderCalendarExport(ICalFeed): """Calendar export for Purchase/Sales Orders @@ -1450,6 +1550,16 @@ order_api_urls = [ re_path(r'^.*$', SalesOrderAllocationList.as_view(), name='api-so-allocation-list'), ])), + # API endpoints for return orders + re_path(r'^return/', include([ + + # Return Order detail + path('/', ReturnOrderDetail.as_view(), name='api-return-order-detail'), + + # Return Order list + re_path(r'^.*$', ReturnOrderList.as_view(), name='api-return-order-list'), + ])), + # API endpoint for subscribing to ICS calendar of purchase/sales orders re_path(r'^calendar/(?Ppurchase-order|sales-order)/calendar.ics', OrderCalendarExport(), name='api-po-so-calendar'), ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 1ddc63a2df..109b2045fa 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -46,9 +46,13 @@ from users import models as UserModels logger = logging.getLogger('inventree') -class TotalPriceMixin: +class TotalPriceMixin(models.Model): """Mixin which provides 'total_price' field for an order""" + class Meta: + """Meta for MetadataMixin.""" + abstract = True + def save(self, *args, **kwargs): """Update the total_price field when saved""" @@ -1573,7 +1577,7 @@ class ReturnOrder(Order): @staticmethod def get_api_url(): """Return the API URL associated with the ReturnOrder model""" - return reverse('api-return-list') + return reverse('api-return-order-list') @classmethod def api_defaults(cls, request): @@ -1645,7 +1649,7 @@ class ReturnOrderAttachment(InvenTreeAttachment): def get_api_url(): """Return the API URL associated with the ReturnOrderAttachment class""" - return reverse('api-return-attachment-list') + return reverse('api-return-order-attachment-list') def getSubdir(self): """Return the directory path where ReturnOrderAttachment files are located""" diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index b0627156b1..8aa59ccb5c 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -645,7 +645,7 @@ class PurchaseOrderAttachmentSerializer(InvenTreeAttachmentSerializer): class SalesOrderSerializer(AbstractOrderSerializer, InvenTreeModelSerializer): - """Serializers for the SalesOrder object.""" + """Serializer for the SalesOrder model class""" class Meta: """Metaclass options.""" @@ -1398,3 +1398,78 @@ class SalesOrderAttachmentSerializer(InvenTreeAttachmentSerializer): fields = InvenTreeAttachmentSerializer.attachment_fields([ 'order', ]) + + +class ReturnOrderSerializer(InvenTreeModelSerializer): + """Serializer for the ReturnOrder model class""" + + class Meta: + """Metaclass options""" + + model = order.models.ReturnOrder + + fields = [ + 'pk', + 'creation_date', + 'customer', + 'customer_detail', + 'customer_reference', + 'description', + 'link', + 'notes', + 'reference', + 'responsible', + 'responsible_detail', + 'status', + 'status_text', + ] + + read_only_fields = [ + 'status', + 'creation_date', + ] + + def __init__(self, *args, **kwargs): + """Initialization routine for the serializer""" + + customer_detail = kwargs.pop('customer_detail', False) + + super().__init__(*args, **kwargs) + + if customer_detail is not True: + self.fields.pop('customer_detail') + + @staticmethod + def annotate_queryset(queryset): + """Custom annotation for the serializer queryset""" + + # TODO + return queryset + + customer_detail = CompanyBriefSerializer(source='customer', many=False, read_only=True) + + status_text = serializers.CharField(source='get_status_display', read_only=True) + + responsible_detail = OwnerSerializer(source='responsible', read_only=True, many=False) + + reference = serializers.CharField(required=True) + + def validate_reference(self, reference): + """Custom validation for the reference field""" + + order.models.ReturnOrder.validate_reference_field(reference) + + return reference + + +class ReturnOrderAttachmentSerializer(InvenTreeAttachmentSerializer): + """Serializer for the ReturnOrderAttachment model""" + + class Meta: + """Metaclass options""" + + model = order.models.ReturnOrderAttachment + + fields = InvenTreeAttachmentSerializer.attachment_fields([ + 'order', + ])