<commit_before>"""
JSON API for the Stock app
"""

from django.conf.urls import url, include
from django.urls import reverse
from django.http import JsonResponse
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _

from rest_framework import status
from rest_framework.serializers import ValidationError
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import generics, filters, permissions

from django_filters.rest_framework import DjangoFilterBackend
from django_filters import rest_framework as rest_filters

from .models import StockLocation, StockItem
from .models import StockItemTracking
from .models import StockItemAttachment
from .models import StockItemTestResult

from part.models import Part, PartCategory
from part.serializers import PartBriefSerializer

from company.models import Company, SupplierPart
from company.serializers import CompanySerializer, SupplierPartSerializer

from order.models import PurchaseOrder
from order.serializers import POSerializer

import common.settings
import common.models

from .serializers import StockItemSerializer
from .serializers import LocationSerializer, LocationBriefSerializer
from .serializers import StockTrackingSerializer
from .serializers import StockItemAttachmentSerializer
from .serializers import StockItemTestResultSerializer

from InvenTree.views import TreeSerializer
from InvenTree.helpers import str2bool, isNull
from InvenTree.api import AttachmentMixin

from decimal import Decimal, InvalidOperation

from datetime import datetime, timedelta


class StockCategoryTree(TreeSerializer):
    title = _('Stock')
    model = StockLocation

    @property
    def root_url(self):
        return reverse('stock-index')

    def get_items(self):
        return StockLocation.objects.all().prefetch_related('stock_items', 'children')

    permission_classes = [
        permissions.IsAuthenticated,
    ]


class StockDetail(generics.RetrieveUpdateDestroyAPIView):
    """ API detail endpoint for Stock object

    get:
    Return a single StockItem object

    post:
    Update a StockItem

    delete:
    Remove a StockItem
    """

    queryset = StockItem.objects.all()
    serializer_class = StockItemSerializer

    def get_queryset(self, *args, **kwargs):

        queryset = super().get_queryset(*args, **kwargs)
        queryset = StockItemSerializer.prefetch_queryset(queryset)
        queryset = StockItemSerializer.annotate_queryset(queryset)

        return queryset

    def get_serializer(self, *args, **kwargs):

        kwargs['part_detail'] = True
        kwargs['location_detail'] = True
        kwargs['supplier_part_detail'] = True
        kwargs['test_detail'] = True
        kwargs['context'] = self.get_serializer_context()

        return self.serializer_class(*args, **kwargs)

    def update(self, request, *args, **kwargs):
        """
        Record the user who updated the item
        """

        # TODO: Record the user!
        # user = request.user

        return super().update(request, *args, **kwargs)


class StockAdjust(APIView):
    """
    A generic class for handling stocktake actions.

    Subclasses exist for:

    - StockCount: count stock items
    - StockAdd: add stock items
    - StockRemove: remove stock items
    - StockTransfer: transfer stock items

    # TODO - This needs serious refactoring!!!

    """

    queryset = StockItem.objects.none()

    allow_missing_quantity = False

    def get_items(self, request):
        """
        Return a list of items posted to the endpoint.
        Will raise validation errors if the items are not
        correctly formatted.
        """

        _items = []

        if 'item' in request.data:
            _items = [request.data['item']]
        elif 'items' in request.data:
            _items = request.data['items']
        else:
            _items = []

        if len(_items) == 0:
            raise ValidationError(_('Request must contain list of stock items'))

        # List of validated items
        self.items = []

        # List of error messages
        errors = []

        for entry in _items:

            if not type(entry) == dict:
                raise ValidationError(_('Improperly formatted data'))

            # Look for a 'pk' value (use 'id' as a backup)
            pk = entry.get('pk', entry.get('id', None))

            try:
                pk = int(pk)
            except ValueError:
                raise ValidationError(_('Each entry must contain a valid integer primary-key'))

            try:
                item = StockItem.objects.get(pk=pk)
            except (StockItem.DoesNotExist):
                raise ValidationError({
                    pk: [_('Primary key does not match valid stock item')]
                })

            if self.allow_missing_quantity and 'quantity' not in entry:
                entry['quantity'] = item.quantity

            try:
                quantity = Decimal(str(entry.get('quantity', None)))
            except (ValueError, TypeError, InvalidOperation):
                raise ValidationError({
                    pk: [_('Invalid quantity value')]
                })

            if quantity < 0:
                raise ValidationError({
                    pk: [_('Quantity must not be less than zero')]
                })

            self.items.append({
                'item': item,
                'quantity': quantity
            })

        # Extract 'notes' field
        self.notes = str(request.data.get('notes', ''))


class StockCount(StockAdjust):
    """
    Endpoint for counting stock (performing a stocktake).
    """

    def post(self, request, *args, **kwargs):

        self.get_items(request)

        n = 0

        for item in self.items:

            if item['item'].stocktake(item['quantity'], request.user, notes=self.notes):
                n += 1

        return Response({'success': _('Updated stock for {n} items').format(n=n)})


class StockAdd(StockAdjust):
    """
    Endpoint for adding a quantity of stock to an existing StockItem
    """

    def post(self, request, *args, **kwargs):

        self.get_items(request)

        n = 0

        for item in self.items:
            if item['item'].add_stock(item['quantity'], request.user, notes=self.notes):
                n += 1

        return Response({"success": "Added stock for {n} items".format(n=n)})


class StockRemove(StockAdjust):
    """
    Endpoint for removing a quantity of stock from an existing StockItem.
    """

    def post(self, request, *args, **kwargs):

        self.get_items(request)

        n = 0

        for item in self.items:

            if item['item'].take_stock(item['quantity'], request.user, notes=self.notes):
                n += 1

        return Response({"success": "Removed stock for {n} items".format(n=n)})


class StockTransfer(StockAdjust):
    """
    API endpoint for performing stock movements
    """

    allow_missing_quantity = True

    def post(self, request, *args, **kwargs):

        data = request.data

        try:
            location = StockLocation.objects.get(pk=data.get('location', None))
        except (ValueError, StockLocation.DoesNotExist):
            raise ValidationError({'location': [_('Valid location must be specified')]})

        n = 0

        self.get_items(request)

        for item in self.items:

            # If quantity is not specified, move the entire stock<commit_msg>Raise error if specified quantity is above available quantity<commit_after>"""
JSON API for the Stock app
"""

from django.conf.urls import url, include
from django.urls import reverse
from django.http import JsonResponse
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _

from rest_framework import status
from rest_framework.serializers import ValidationError
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import generics, filters, permissions

from django_filters.rest_framework import DjangoFilterBackend
from django_filters import rest_framework as rest_filters

from .models import StockLocation, StockItem
from .models import StockItemTracking
from .models import StockItemAttachment
from .models import StockItemTestResult

from part.models import Part, PartCategory
from part.serializers import PartBriefSerializer

from company.models import Company, SupplierPart
from company.serializers import CompanySerializer, SupplierPartSerializer

from order.models import PurchaseOrder
from order.serializers import POSerializer

import common.settings
import common.models

from .serializers import StockItemSerializer
from .serializers import LocationSerializer, LocationBriefSerializer
from .serializers import StockTrackingSerializer
from .serializers import StockItemAttachmentSerializer
from .serializers import StockItemTestResultSerializer

from InvenTree.views import TreeSerializer
from InvenTree.helpers import str2bool, isNull
from InvenTree.api import AttachmentMixin

from decimal import Decimal, InvalidOperation

from datetime import datetime, timedelta


class StockCategoryTree(TreeSerializer):
    title = _('Stock')
    model = StockLocation

    @property
    def root_url(self):
        return reverse('stock-index')

    def get_items(self):
        return StockLocation.objects.all().prefetch_related('stock_items', 'children')

    permission_classes = [
        permissions.IsAuthenticated,
    ]


class StockDetail(generics.RetrieveUpdateDestroyAPIView):
    """ API detail endpoint for Stock object

    get:
    Return a single StockItem object

    post:
    Update a StockItem

    delete:
    Remove a StockItem
    """

    queryset = StockItem.objects.all()
    serializer_class = StockItemSerializer

    def get_queryset(self, *args, **kwargs):

        queryset = super().get_queryset(*args, **kwargs)
        queryset = StockItemSerializer.prefetch_queryset(queryset)
        queryset = StockItemSerializer.annotate_queryset(queryset)

        return queryset

    def get_serializer(self, *args, **kwargs):

        kwargs['part_detail'] = True
        kwargs['location_detail'] = True
        kwargs['supplier_part_detail'] = True
        kwargs['test_detail'] = True
        kwargs['context'] = self.get_serializer_context()

        return self.serializer_class(*args, **kwargs)

    def update(self, request, *args, **kwargs):
        """
        Record the user who updated the item
        """

        # TODO: Record the user!
        # user = request.user

        return super().update(request, *args, **kwargs)


class StockAdjust(APIView):
    """
    A generic class for handling stocktake actions.

    Subclasses exist for:

    - StockCount: count stock items
    - StockAdd: add stock items
    - StockRemove: remove stock items
    - StockTransfer: transfer stock items

    # TODO - This needs serious refactoring!!!

    """

    queryset = StockItem.objects.none()

    allow_missing_quantity = False

    def get_items(self, request):
        """
        Return a list of items posted to the endpoint.
        Will raise validation errors if the items are not
        correctly formatted.
        """

        _items = []

        if 'item' in request.data:
            _items = [request.data['item']]
        elif 'items' in request.data:
            _items = request.data['items']
        else:
            _items = []

        if len(_items) == 0:
            raise ValidationError(_('Request must contain list of stock items'))

        # List of validated items
        self.items = []

        # List of error messages
        errors = []

        for entry in _items:

            if not type(entry) == dict:
                raise ValidationError(_('Improperly formatted data'))

            # Look for a 'pk' value (use 'id' as a backup)
            pk = entry.get('pk', entry.get('id', None))

            try:
                pk = int(pk)
            except ValueError:
                raise ValidationError(_('Each entry must contain a valid integer primary-key'))

            try:
                item = StockItem.objects.get(pk=pk)
            except (StockItem.DoesNotExist):
                raise ValidationError({
                    pk: [_('Primary key does not match valid stock item')]
                })

            if self.allow_missing_quantity and 'quantity' not in entry:
                entry['quantity'] = item.quantity

            try:
                quantity = Decimal(str(entry.get('quantity', None)))
            except (ValueError, TypeError, InvalidOperation):
                raise ValidationError({
                    pk: [_('Invalid quantity value')]
                })

            if quantity < 0:
                raise ValidationError({
                    pk: [_('Quantity must not be less than zero')]
                })

            self.items.append({
                'item': item,
                'quantity': quantity
            })

        # Extract 'notes' field
        self.notes = str(request.data.get('notes', ''))


class StockCount(StockAdjust):
    """
    Endpoint for counting stock (performing a stocktake).
    """

    def post(self, request, *args, **kwargs):

        self.get_items(request)

        n = 0

        for item in self.items:

            if item['item'].stocktake(item['quantity'], request.user, notes=self.notes):
                n += 1

        return Response({'success': _('Updated stock for {n} items').format(n=n)})


class StockAdd(StockAdjust):
    """
    Endpoint for adding a quantity of stock to an existing StockItem
    """

    def post(self, request, *args, **kwargs):

        self.get_items(request)

        n = 0

        for item in self.items:
            if item['item'].add_stock(item['quantity'], request.user, notes=self.notes):
                n += 1

        return Response({"success": "Added stock for {n} items".format(n=n)})


class StockRemove(StockAdjust):
    """
    Endpoint for removing a quantity of stock from an existing StockItem.
    """

    def post(self, request, *args, **kwargs):

        self.get_items(request)

        n = 0

        for item in self.items:

            if item['quantity'] > item['item'].quantity:
                raise ValidationError({
                    item['item'].pk: [_('Specified quantity exceeds stock quantity')]
                })

            if item['item'].take_stock(item['quantity'], request.user, notes=self.notes):
                n += 1

        return Response({"success": "Removed stock for {n} items".format(n=n)})


class StockTransfer(StockAdjust):
    """
    API endpoint for performing stock movements
    """

    allow_missing_quantity = True

    def post(self, request, *args, **kwargs):

        data = request.data

        try:
            location = StockLocation.objects.get(pk=data.get('location', None))
        except (ValueError, StockLocation.DoesNotExist):
            raise ValidationError({'location': [_('Valid location must be specified')]})

        n = 0

        self.get_items(request)

        for item in self.items:

            if item['quantity'] > item['item'].quantity:
                raise ValidationError({
                    item['item'].pk: [_('Specified quantity exceeds stock quantity')]
                })

            # If quantity is not specified, move the entire stock
{'fraction_non_alphanumeric': 0.09234555109441782, 'fraction_numerical': 0.0012951690195570523, 'mean_word_length': 2.5121673868546734, 'pattern_counts': {'":': 4, '<': 5, '<?xml version=': 0, '>': 5, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4402121', 'n_tokens_mistral': 4150, 'n_tokens_neox': 3855, 'n_words': 1237}