2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-01 03:00:54 +00:00

Docstring checks in QC checks (#3089)

* Add pre-commit to the stack

* exclude static

* Add locales to excludes

* fix style errors

* rename pipeline steps

* also wait on precommit

* make template matching simpler

* Use the same code for python setup everywhere

* use step and cache for python setup

* move regular settings up into general envs

* just use full update

* Use invoke instead of static references

* make setup actions more similar

* use python3

* refactor names to be similar

* fix runner version

* fix references

* remove incidential change

* use matrix for os

* Github can't do this right now

* ignore docstyle errors

* Add seperate docstring test

* update flake call

* do not fail on docstring

* refactor setup into workflow

* update reference

* switch to action

* resturcture

* add bash statements

* remove os from cache

* update input checks

* make code cleaner

* fix boolean

* no relative paths

* install wheel by python

* switch to install

* revert back to simple wheel

* refactor import export tests

* move setup keys back to not disturbe tests

* remove docstyle till that is fixed

* update references

* continue on error

* add docstring test

* use relativ action references

* Change step / job docstrings

* update to merge

* reformat comments 1

* fix docstrings 2

* fix docstrings 3

* fix docstrings 4

* fix docstrings 5

* fix docstrings 6

* fix docstrings 7

* fix docstrings 8

* fix docstirns 9

* fix docstrings 10

* docstring adjustments

* update the remaining docstrings

* small docstring changes

* fix function name

* update support files for docstrings

* Add missing args to docstrings

* Remove outdated function

* Add docstrings for the 'build' app

* Make API code cleaner

* add more docstrings for plugin app

* Remove dead code for plugin settings
No idea what that was even intended for

* ignore __init__ files for docstrings

* More docstrings

* Update docstrings for the 'part' directory

* Fixes for related_part functionality

* Fix removed stuff from merge 99676ee

* make more consistent

* Show statistics for docstrings

* add more docstrings

* move specific register statements to make them clearer to understant

* More docstrings for common

* and more docstrings

* and more

* simpler call

* docstrings for notifications

* docstrings for common/tests

* Add docs for common/models

* Revert "move specific register statements to make them clearer to understant"

This reverts commit ca96654622.

* use typing here

* Revert "Make API code cleaner"

This reverts commit 24fb68bd3e.

* docstring updates for the 'users' app

* Add generic Meta info to simple Meta classes

* remove unneeded unique_together statements

* More simple metas

* Remove unnecessary format specifier

* Remove extra json format specifiers

* Add docstrings for the 'plugin' app

* Docstrings for the 'label' app

* Add missing docstrings for the 'report' app

* Fix build test regression

* Fix top-level files

* docstrings for InvenTree/InvenTree

* reduce unneeded code

* add docstrings

* and more docstrings

* more docstrings

* more docstrings for stock

* more docstrings

* docstrings for order/views

* Docstrings for various files in the 'order' app

* Docstrings for order/test_api.py

* Docstrings for order/serializers.py

* Docstrings for order/admin.py

* More docstrings for the order app

* Add docstrings for the 'company' app

* Add unit tests for rebuilding the reference fields

* Prune out some more dead code

* remove more dead code

Co-authored-by: Oliver Walters <oliver.henry.walters@gmail.com>
This commit is contained in:
Matthias Mair
2022-06-01 17:37:39 +02:00
committed by GitHub
parent 66a6915213
commit 0c97a50e47
223 changed files with 4416 additions and 6980 deletions

View File

@ -1,5 +1,4 @@
"""
The Stock module is responsible for Stock management.
"""The Stock module is responsible for Stock management.
It includes models for:

View File

@ -1,3 +1,5 @@
"""Admin for stock app."""
from django.contrib import admin
import import_export.widgets as widgets
@ -15,13 +17,15 @@ from .models import (StockItem, StockItemAttachment, StockItemTestResult,
class LocationResource(ModelResource):
""" Class for managing StockLocation data import/export """
"""Class for managing StockLocation data import/export."""
parent = Field(attribute='parent', widget=widgets.ForeignKeyWidget(StockLocation))
parent_name = Field(attribute='parent__name', readonly=True)
class Meta:
"""Metaclass options."""
model = StockLocation
skip_unchanged = True
report_skipped = False
@ -34,7 +38,7 @@ class LocationResource(ModelResource):
]
def after_import(self, dataset, result, using_transactions, dry_run, **kwargs):
"""Rebuild after import to keep tree intact."""
super().after_import(dataset, result, using_transactions, dry_run, **kwargs)
# Rebuild the StockLocation tree(s)
@ -42,13 +46,12 @@ class LocationResource(ModelResource):
class LocationInline(admin.TabularInline):
"""
Inline for sub-locations
"""
"""Inline for sub-locations."""
model = StockLocation
class LocationAdmin(ImportExportModelAdmin):
"""Admin class for Location."""
resource_class = LocationResource
@ -66,7 +69,7 @@ class LocationAdmin(ImportExportModelAdmin):
class StockItemResource(ModelResource):
""" Class for managing StockItem data import/export """
"""Class for managing StockItem data import/export."""
# Custom managers for ForeignKey fields
part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part))
@ -103,13 +106,15 @@ class StockItemResource(ModelResource):
stocktake_date = Field(attribute='stocktake_date', widget=widgets.DateWidget())
def after_import(self, dataset, result, using_transactions, dry_run, **kwargs):
"""Rebuild after import to keep tree intact."""
super().after_import(dataset, result, using_transactions, dry_run, **kwargs)
# Rebuild the StockItem tree(s)
StockItem.objects.rebuild()
class Meta:
"""Metaclass options."""
model = StockItem
skip_unchanged = True
report_skipped = False
@ -124,6 +129,7 @@ class StockItemResource(ModelResource):
class StockItemAdmin(ImportExportModelAdmin):
"""Admin class for StockItem."""
resource_class = StockItemResource
@ -152,6 +158,7 @@ class StockItemAdmin(ImportExportModelAdmin):
class StockAttachmentAdmin(admin.ModelAdmin):
"""Admin class for StockAttachment."""
list_display = ('stock_item', 'attachment', 'comment')
@ -161,6 +168,8 @@ class StockAttachmentAdmin(admin.ModelAdmin):
class StockTrackingAdmin(ImportExportModelAdmin):
"""Admin class for StockTracking."""
list_display = ('item', 'date', 'label')
autocomplete_fields = [
@ -169,6 +178,7 @@ class StockTrackingAdmin(ImportExportModelAdmin):
class StockItemTestResultAdmin(admin.ModelAdmin):
"""Admin class for StockItemTestResult."""
list_display = ('stock_item', 'test', 'result', 'value')

View File

@ -1,6 +1,4 @@
"""
JSON API for the Stock app
"""
"""JSON API for the Stock app."""
from collections import OrderedDict
from datetime import datetime, timedelta
@ -39,7 +37,7 @@ from stock.models import (StockItem, StockItemAttachment, StockItemTestResult,
class StockDetail(generics.RetrieveUpdateDestroyAPIView):
""" API detail endpoint for Stock object
"""API detail endpoint for Stock object.
get:
Return a single StockItem object
@ -55,21 +53,21 @@ class StockDetail(generics.RetrieveUpdateDestroyAPIView):
serializer_class = StockSerializers.StockItemSerializer
def get_queryset(self, *args, **kwargs):
"""Annotate queryset."""
queryset = super().get_queryset(*args, **kwargs)
queryset = StockSerializers.StockItemSerializer.annotate_queryset(queryset)
return queryset
def get_serializer_context(self):
"""Extend serializer context."""
ctx = super().get_serializer_context()
ctx['user'] = getattr(self.request, 'user', None)
return ctx
def get_serializer(self, *args, **kwargs):
"""Set context before returning serializer."""
kwargs['part_detail'] = True
kwargs['location_detail'] = True
kwargs['supplier_part_detail'] = True
@ -80,19 +78,20 @@ class StockDetail(generics.RetrieveUpdateDestroyAPIView):
class StockMetadata(generics.RetrieveUpdateAPIView):
"""API endpoint for viewing / updating StockItem metadata"""
"""API endpoint for viewing / updating StockItem metadata."""
def get_serializer(self, *args, **kwargs):
"""Return serializer."""
return MetadataSerializer(StockItem, *args, **kwargs)
queryset = StockItem.objects.all()
class StockItemContextMixin:
""" Mixin class for adding StockItem object to serializer context """
"""Mixin class for adding StockItem object to serializer context."""
def get_serializer_context(self):
"""Extend serializer context."""
context = super().get_serializer_context()
context['request'] = self.request
@ -105,17 +104,14 @@ class StockItemContextMixin:
class StockItemSerialize(StockItemContextMixin, generics.CreateAPIView):
"""
API endpoint for serializing a stock item
"""
"""API endpoint for serializing a stock item."""
queryset = StockItem.objects.none()
serializer_class = StockSerializers.SerializeStockItemSerializer
class StockItemInstall(StockItemContextMixin, generics.CreateAPIView):
"""
API endpoint for installing a particular stock item into this stock item.
"""API endpoint for installing a particular stock item into this stock item.
- stock_item.part must be in the BOM for this part
- stock_item must currently be "in stock"
@ -127,17 +123,14 @@ class StockItemInstall(StockItemContextMixin, generics.CreateAPIView):
class StockItemUninstall(StockItemContextMixin, generics.CreateAPIView):
"""
API endpoint for removing (uninstalling) items from this item
"""
"""API endpoint for removing (uninstalling) items from this item."""
queryset = StockItem.objects.none()
serializer_class = StockSerializers.UninstallStockItemSerializer
class StockAdjustView(generics.CreateAPIView):
"""
A generic class for handling stocktake actions.
"""A generic class for handling stocktake actions.
Subclasses exist for:
@ -150,80 +143,66 @@ class StockAdjustView(generics.CreateAPIView):
queryset = StockItem.objects.none()
def get_serializer_context(self):
"""Extend serializer context."""
context = super().get_serializer_context()
context['request'] = self.request
return context
class StockCount(StockAdjustView):
"""
Endpoint for counting stock (performing a stocktake).
"""
"""Endpoint for counting stock (performing a stocktake)."""
serializer_class = StockSerializers.StockCountSerializer
class StockAdd(StockAdjustView):
"""
Endpoint for adding a quantity of stock to an existing StockItem
"""
"""Endpoint for adding a quantity of stock to an existing StockItem."""
serializer_class = StockSerializers.StockAddSerializer
class StockRemove(StockAdjustView):
"""
Endpoint for removing a quantity of stock from an existing StockItem.
"""
"""Endpoint for removing a quantity of stock from an existing StockItem."""
serializer_class = StockSerializers.StockRemoveSerializer
class StockTransfer(StockAdjustView):
"""
API endpoint for performing stock movements
"""
"""API endpoint for performing stock movements."""
serializer_class = StockSerializers.StockTransferSerializer
class StockAssign(generics.CreateAPIView):
"""
API endpoint for assigning stock to a particular customer
"""
"""API endpoint for assigning stock to a particular customer."""
queryset = StockItem.objects.all()
serializer_class = StockSerializers.StockAssignmentSerializer
def get_serializer_context(self):
"""Extend serializer context."""
ctx = super().get_serializer_context()
ctx['request'] = self.request
return ctx
class StockMerge(generics.CreateAPIView):
"""
API endpoint for merging multiple stock items
"""
"""API endpoint for merging multiple stock items."""
queryset = StockItem.objects.none()
serializer_class = StockSerializers.StockMergeSerializer
def get_serializer_context(self):
"""Extend serializer context."""
ctx = super().get_serializer_context()
ctx['request'] = self.request
return ctx
class StockLocationList(generics.ListCreateAPIView):
"""
API endpoint for list view of StockLocation objects:
"""API endpoint for list view of StockLocation objects.
- GET: Return list of StockLocation objects
- POST: Create a new StockLocation
@ -233,11 +212,7 @@ class StockLocationList(generics.ListCreateAPIView):
serializer_class = StockSerializers.LocationSerializer
def filter_queryset(self, queryset):
"""
Custom filtering:
- Allow filtering by "null" parent to retrieve top-level stock locations
"""
"""Custom filtering: - Allow filtering by "null" parent to retrieve top-level stock locations."""
queryset = super().filter_queryset(queryset)
params = self.request.query_params
@ -319,10 +294,7 @@ class StockLocationList(generics.ListCreateAPIView):
class StockLocationTree(generics.ListAPIView):
"""
API endpoint for accessing a list of StockLocation objects,
ready for rendering as a tree
"""
"""API endpoint for accessing a list of StockLocation objects, ready for rendering as a tree."""
queryset = StockLocation.objects.all()
serializer_class = StockSerializers.LocationTreeSerializer
@ -337,9 +309,7 @@ class StockLocationTree(generics.ListAPIView):
class StockFilter(rest_filters.FilterSet):
"""
FilterSet for StockItem LIST API
"""
"""FilterSet for StockItem LIST API."""
# Part name filters
name = rest_filters.CharFilter(label='Part name (case insensitive)', field_name='part__name', lookup_expr='iexact')
@ -361,7 +331,7 @@ class StockFilter(rest_filters.FilterSet):
in_stock = rest_filters.BooleanFilter(label='In Stock', method='filter_in_stock')
def filter_in_stock(self, queryset, name, value):
"""Filter by if item is in stock."""
if str2bool(value):
queryset = queryset.filter(StockItem.IN_STOCK_FILTER)
else:
@ -372,12 +342,10 @@ class StockFilter(rest_filters.FilterSet):
available = rest_filters.BooleanFilter(label='Available', method='filter_available')
def filter_available(self, queryset, name, value):
"""
Filter by whether the StockItem is "available" or not.
"""Filter by whether the StockItem is "available" or not.
Here, "available" means that the allocated quantity is less than the total quantity
"""
if str2bool(value):
# The 'quantity' field is greater than the calculated 'allocated' field
queryset = queryset.filter(Q(quantity__gt=F('allocated')))
@ -401,10 +369,7 @@ class StockFilter(rest_filters.FilterSet):
serialized = rest_filters.BooleanFilter(label='Has serial number', method='filter_serialized')
def filter_serialized(self, queryset, name, value):
"""
Filter by whether the StockItem has a serial number (or not)
"""
"""Filter by whether the StockItem has a serial number (or not)."""
q = Q(serial=None) | Q(serial='')
if str2bool(value):
@ -417,10 +382,7 @@ class StockFilter(rest_filters.FilterSet):
has_batch = rest_filters.BooleanFilter(label='Has batch code', method='filter_has_batch')
def filter_has_batch(self, queryset, name, value):
"""
Filter by whether the StockItem has a batch code (or not)
"""
"""Filter by whether the StockItem has a batch code (or not)."""
q = Q(batch=None) | Q(batch='')
if str2bool(value):
@ -433,12 +395,12 @@ class StockFilter(rest_filters.FilterSet):
tracked = rest_filters.BooleanFilter(label='Tracked', method='filter_tracked')
def filter_tracked(self, queryset, name, value):
"""
Filter by whether this stock item is *tracked*, meaning either:
"""Filter by whether this stock item is *tracked*.
Meaning either:
- It has a serial number
- It has a batch code
"""
q_batch = Q(batch=None) | Q(batch='')
q_serial = Q(serial=None) | Q(serial='')
@ -452,10 +414,7 @@ class StockFilter(rest_filters.FilterSet):
installed = rest_filters.BooleanFilter(label='Installed in other stock item', method='filter_installed')
def filter_installed(self, queryset, name, value):
"""
Filter stock items by "belongs_to" field being empty
"""
"""Filter stock items by "belongs_to" field being empty."""
if str2bool(value):
queryset = queryset.exclude(belongs_to=None)
else:
@ -466,7 +425,7 @@ class StockFilter(rest_filters.FilterSet):
sent_to_customer = rest_filters.BooleanFilter(label='Sent to customer', method='filter_sent_to_customer')
def filter_sent_to_customer(self, queryset, name, value):
"""Filter by sent to customer."""
if str2bool(value):
queryset = queryset.exclude(customer=None)
else:
@ -477,7 +436,7 @@ class StockFilter(rest_filters.FilterSet):
depleted = rest_filters.BooleanFilter(label='Depleted', method='filter_depleted')
def filter_depleted(self, queryset, name, value):
"""Filter by depleted items."""
if str2bool(value):
queryset = queryset.filter(quantity__lte=0)
else:
@ -488,9 +447,9 @@ class StockFilter(rest_filters.FilterSet):
has_purchase_price = rest_filters.BooleanFilter(label='Has purchase price', method='filter_has_purchase_price')
def filter_has_purchase_price(self, queryset, name, value):
"""Filter by having a purchase price."""
if str2bool(value):
queryset = queryset.exclude(purcahse_price=None)
queryset = queryset.exclude(purchase_price=None)
else:
queryset = queryset.filter(purchase_price=None)
@ -502,7 +461,7 @@ class StockFilter(rest_filters.FilterSet):
class StockList(APIDownloadMixin, generics.ListCreateAPIView):
""" API endpoint for list view of Stock objects
"""API endpoint for list view of Stock objects.
- GET: Return a list of all StockItem objects (with optional query filters)
- POST: Create a new StockItem
@ -513,22 +472,20 @@ class StockList(APIDownloadMixin, generics.ListCreateAPIView):
filterset_class = StockFilter
def get_serializer_context(self):
"""Extend serializer context."""
ctx = super().get_serializer_context()
ctx['user'] = getattr(self.request, 'user', None)
return ctx
def create(self, request, *args, **kwargs):
"""
Create a new StockItem object via the API.
"""Create a new StockItem object via the API.
We override the default 'create' implementation.
If a location is *not* specified, but the linked *part* has a default location,
we can pre-fill the location automatically.
"""
user = request.user
# Copy the request data, to side-step "mutability" issues
@ -602,9 +559,7 @@ class StockList(APIDownloadMixin, generics.ListCreateAPIView):
})
if serials is not None:
"""
If the stock item is going to be serialized, set the quantity to 1
"""
"""If the stock item is going to be serialized, set the quantity to 1."""
data['quantity'] = 1
# De-serialize the provided data
@ -643,8 +598,8 @@ class StockList(APIDownloadMixin, generics.ListCreateAPIView):
return Response(response_data, status=status.HTTP_201_CREATED, headers=self.get_success_headers(serializer.data))
def download_queryset(self, queryset, export_format):
"""
Download this queryset as a file.
"""Download this queryset as a file.
Uses the APIDownloadMixin mixin class
"""
dataset = StockItemResource().export(queryset=queryset)
@ -659,13 +614,10 @@ class StockList(APIDownloadMixin, generics.ListCreateAPIView):
return DownloadFile(filedata, filename)
def list(self, request, *args, **kwargs):
"""
Override the 'list' method, as the StockLocation objects
are very expensive to serialize.
"""Override the 'list' method, as the StockLocation objects are very expensive to serialize.
So, we fetch and serialize the required StockLocation objects only as required.
"""
queryset = self.filter_queryset(self.get_queryset())
params = request.query_params
@ -767,7 +719,7 @@ class StockList(APIDownloadMixin, generics.ListCreateAPIView):
return Response(data)
def get_queryset(self, *args, **kwargs):
"""Annotate queryset before returning."""
queryset = super().get_queryset(*args, **kwargs)
queryset = StockSerializers.StockItemSerializer.annotate_queryset(queryset)
@ -775,10 +727,7 @@ class StockList(APIDownloadMixin, generics.ListCreateAPIView):
return queryset
def filter_queryset(self, queryset):
"""
Custom filtering for the StockItem queryset
"""
"""Custom filtering for the StockItem queryset."""
params = self.request.query_params
queryset = super().filter_queryset(queryset)
@ -1090,9 +1039,7 @@ class StockList(APIDownloadMixin, generics.ListCreateAPIView):
class StockAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
"""
API endpoint for listing (and creating) a StockItemAttachment (file upload)
"""
"""API endpoint for listing (and creating) a StockItemAttachment (file upload)."""
queryset = StockItemAttachment.objects.all()
serializer_class = StockSerializers.StockItemAttachmentSerializer
@ -1109,27 +1056,21 @@ class StockAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
class StockAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin):
"""
Detail endpoint for StockItemAttachment
"""
"""Detail endpoint for StockItemAttachment."""
queryset = StockItemAttachment.objects.all()
serializer_class = StockSerializers.StockItemAttachmentSerializer
class StockItemTestResultDetail(generics.RetrieveUpdateDestroyAPIView):
"""
Detail endpoint for StockItemTestResult
"""
"""Detail endpoint for StockItemTestResult."""
queryset = StockItemTestResult.objects.all()
serializer_class = StockSerializers.StockItemTestResultSerializer
class StockItemTestResultList(generics.ListCreateAPIView):
"""
API endpoint for listing (and creating) a StockItemTestResult object.
"""
"""API endpoint for listing (and creating) a StockItemTestResult object."""
queryset = StockItemTestResult.objects.all()
serializer_class = StockSerializers.StockItemTestResultSerializer
@ -1150,7 +1091,7 @@ class StockItemTestResultList(generics.ListCreateAPIView):
ordering = 'date'
def filter_queryset(self, queryset):
"""Filter by build or stock_item."""
params = self.request.query_params
queryset = super().filter_queryset(queryset)
@ -1195,6 +1136,7 @@ class StockItemTestResultList(generics.ListCreateAPIView):
return queryset
def get_serializer(self, *args, **kwargs):
"""Set context before returning serializer."""
try:
kwargs['user_detail'] = str2bool(self.request.query_params.get('user_detail', False))
except:
@ -1205,13 +1147,11 @@ class StockItemTestResultList(generics.ListCreateAPIView):
return self.serializer_class(*args, **kwargs)
def perform_create(self, serializer):
"""
Create a new test result object.
"""Create a new test result object.
Also, check if an attachment was uploaded alongside the test result,
and save it to the database if it were.
"""
# Capture the user information
test_result = serializer.save()
test_result.user = self.request.user
@ -1219,16 +1159,14 @@ class StockItemTestResultList(generics.ListCreateAPIView):
class StockTrackingDetail(generics.RetrieveAPIView):
"""
Detail API endpoint for StockItemTracking model
"""
"""Detail API endpoint for StockItemTracking model."""
queryset = StockItemTracking.objects.all()
serializer_class = StockSerializers.StockTrackingSerializer
class StockTrackingList(generics.ListAPIView):
""" API endpoint for list view of StockItemTracking objects.
"""API endpoint for list view of StockItemTracking objects.
StockItemTracking objects are read-only
(they are created by internal model functionality)
@ -1240,6 +1178,7 @@ class StockTrackingList(generics.ListAPIView):
serializer_class = StockSerializers.StockTrackingSerializer
def get_serializer(self, *args, **kwargs):
"""Set context before returning serializer."""
try:
kwargs['item_detail'] = str2bool(self.request.query_params.get('item_detail', False))
except:
@ -1255,7 +1194,7 @@ class StockTrackingList(generics.ListAPIView):
return self.serializer_class(*args, **kwargs)
def list(self, request, *args, **kwargs):
"""List all stock tracking entries."""
queryset = self.filter_queryset(self.get_queryset())
serializer = self.get_serializer(queryset, many=True)
@ -1320,12 +1259,11 @@ class StockTrackingList(generics.ListAPIView):
return Response(data)
def create(self, request, *args, **kwargs):
""" Create a new StockItemTracking object
"""Create a new StockItemTracking object.
Here we override the default 'create' implementation,
to save the user information associated with the request object.
"""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
@ -1365,16 +1303,17 @@ class StockTrackingList(generics.ListAPIView):
class LocationMetadata(generics.RetrieveUpdateAPIView):
"""API endpoint for viewing / updating StockLocation metadata"""
"""API endpoint for viewing / updating StockLocation metadata."""
def get_serializer(self, *args, **kwargs):
"""Return serializer."""
return MetadataSerializer(StockLocation, *args, **kwargs)
queryset = StockLocation.objects.all()
class LocationDetail(generics.RetrieveUpdateDestroyAPIView):
""" API endpoint for detail view of StockLocation object
"""API endpoint for detail view of StockLocation object.
- GET: Return a single StockLocation object
- PATCH: Update a StockLocation object

View File

@ -1,6 +1,8 @@
"""AppConfig for stock app."""
from django.apps import AppConfig
class StockConfig(AppConfig):
"""AppConfig for stock app."""
name = 'stock'

View File

@ -1,6 +1,4 @@
"""
Django Forms for interacting with Stock app
"""
"""Django Forms for interacting with Stock app."""
from InvenTree.forms import HelperForm
@ -8,13 +6,14 @@ from .models import StockItem, StockItemTracking
class ReturnStockItemForm(HelperForm):
"""
Form for manually returning a StockItem into stock
"""Form for manually returning a StockItem into stock.
TODO: This could be a simple API driven form!
"""
class Meta:
"""Metaclass options."""
model = StockItem
fields = [
'location',
@ -22,13 +21,14 @@ class ReturnStockItemForm(HelperForm):
class ConvertStockItemForm(HelperForm):
"""
Form for converting a StockItem to a variant of its current part.
"""Form for converting a StockItem to a variant of its current part.
TODO: Migrate this form to the modern API forms interface
"""
class Meta:
"""Metaclass options."""
model = StockItem
fields = [
'part'
@ -36,13 +36,14 @@ class ConvertStockItemForm(HelperForm):
class TrackingEntryForm(HelperForm):
"""
Form for creating / editing a StockItemTracking object.
"""Form for creating / editing a StockItemTracking object.
Note: 2021-05-11 - This form is not currently used - should delete?
"""
class Meta:
"""Metaclass options."""
model = StockItemTracking
fields = [

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,4 @@
"""
JSON serializers for Stock app
"""
"""JSON serializers for Stock app."""
from datetime import datetime, timedelta
from decimal import Decimal
@ -29,11 +27,11 @@ from .models import (StockItem, StockItemAttachment, StockItemTestResult,
class LocationBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""
Provides a brief serializer for a StockLocation object
"""
"""Provides a brief serializer for a StockLocation object."""
class Meta:
"""Metaclass options."""
model = StockLocation
fields = [
'pk',
@ -43,7 +41,7 @@ class LocationBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer):
class StockItemSerializerBrief(InvenTree.serializers.InvenTreeModelSerializer):
""" Brief serializers for a StockItem """
"""Brief serializers for a StockItem."""
location_name = serializers.CharField(source='location', read_only=True)
part_name = serializers.CharField(source='part.full_name', read_only=True)
@ -51,6 +49,8 @@ class StockItemSerializerBrief(InvenTree.serializers.InvenTreeModelSerializer):
quantity = InvenTreeDecimalField()
class Meta:
"""Metaclass options."""
model = StockItem
fields = [
'part',
@ -65,34 +65,28 @@ class StockItemSerializerBrief(InvenTree.serializers.InvenTreeModelSerializer):
]
def validate_serial(self, value):
"""Make sure serial is not to big."""
if extract_int(value) > 2147483647:
raise serializers.ValidationError('serial is to to big')
return value
class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
""" Serializer for a StockItem:
"""Serializer for a StockItem.
- Includes serialization for the linked part
- Includes serialization for the item location
"""
def update(self, instance, validated_data):
"""
Custom update method to pass the user information through to the instance
"""
"""Custom update method to pass the user information through to the instance."""
instance._user = self.context['user']
return super().update(instance, validated_data)
@staticmethod
def annotate_queryset(queryset):
"""
Add some extra annotations to the queryset,
performing database queries as efficiently as possible.
"""
"""Add some extra annotations to the queryset, performing database queries as efficiently as possible."""
# Annotate the queryset with the total allocated to sales orders
queryset = queryset.annotate(
allocated=Coalesce(
@ -172,7 +166,7 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
purchase_price_string = serializers.SerializerMethodField()
def get_purchase_price_string(self, obj):
"""Return purchase price as string."""
return str(obj.purchase_price) if obj.purchase_price else '-'
purchase_order_reference = serializers.CharField(source='purchase_order.reference', read_only=True)
@ -180,7 +174,7 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
sales_order_reference = serializers.CharField(source='sales_order.reference', read_only=True)
def __init__(self, *args, **kwargs):
"""Add detail fields."""
part_detail = kwargs.pop('part_detail', False)
location_detail = kwargs.pop('location_detail', False)
supplier_part_detail = kwargs.pop('supplier_part_detail', False)
@ -201,6 +195,8 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
self.fields.pop('required_tests')
class Meta:
"""Metaclass options."""
model = StockItem
fields = [
'allocated',
@ -257,8 +253,7 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
class SerializeStockItemSerializer(serializers.Serializer):
"""
A DRF serializer for "serializing" a StockItem.
"""A DRF serializer for "serializing" a StockItem.
(Sorry for the confusing naming...)
@ -269,6 +264,8 @@ class SerializeStockItemSerializer(serializers.Serializer):
"""
class Meta:
"""Metaclass options."""
fields = [
'quantity',
'serial_numbers',
@ -284,10 +281,7 @@ class SerializeStockItemSerializer(serializers.Serializer):
)
def validate_quantity(self, quantity):
"""
Validate that the quantity value is correct
"""
"""Validate that the quantity value is correct."""
item = self.context['item']
if quantity < 0:
@ -323,10 +317,7 @@ class SerializeStockItemSerializer(serializers.Serializer):
)
def validate(self, data):
"""
Check that the supplied serial numbers are valid
"""
"""Check that the supplied serial numbers are valid."""
data = super().validate(data)
item = self.context['item']
@ -358,7 +349,7 @@ class SerializeStockItemSerializer(serializers.Serializer):
return data
def save(self):
"""Serialize stock item."""
item = self.context['item']
request = self.context['request']
user = request.user
@ -381,9 +372,7 @@ class SerializeStockItemSerializer(serializers.Serializer):
class InstallStockItemSerializer(serializers.Serializer):
"""
Serializer for installing a stock item into a given part
"""
"""Serializer for installing a stock item into a given part."""
stock_item = serializers.PrimaryKeyRelatedField(
queryset=StockItem.objects.all(),
@ -401,10 +390,7 @@ class InstallStockItemSerializer(serializers.Serializer):
)
def validate_stock_item(self, stock_item):
"""
Validate the selected stock item
"""
"""Validate the selected stock item."""
if not stock_item.in_stock:
# StockItem must be in stock to be "installed"
raise ValidationError(_("Stock item is unavailable"))
@ -419,8 +405,7 @@ class InstallStockItemSerializer(serializers.Serializer):
return stock_item
def save(self):
""" Install the selected stock item into this one """
"""Install the selected stock item into this one."""
data = self.validated_data
stock_item = data['stock_item']
@ -438,11 +423,11 @@ class InstallStockItemSerializer(serializers.Serializer):
class UninstallStockItemSerializer(serializers.Serializer):
"""
API serializers for uninstalling an installed item from a stock item
"""
"""API serializers for uninstalling an installed item from a stock item."""
class Meta:
"""Metaclass options."""
fields = [
'location',
'note',
@ -462,7 +447,7 @@ class UninstallStockItemSerializer(serializers.Serializer):
)
def save(self):
"""Uninstall stock item."""
item = self.context['item']
data = self.validated_data
@ -480,11 +465,11 @@ class UninstallStockItemSerializer(serializers.Serializer):
class LocationTreeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
"""
Serializer for a simple tree view
"""
"""Serializer for a simple tree view."""
class Meta:
"""Metaclass options."""
model = StockLocation
fields = [
'pk',
@ -494,8 +479,7 @@ class LocationTreeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
class LocationSerializer(InvenTree.serializers.InvenTreeModelSerializer):
""" Detailed information about a stock location
"""
"""Detailed information about a stock location."""
url = serializers.CharField(source='get_absolute_url', read_only=True)
@ -504,6 +488,8 @@ class LocationSerializer(InvenTree.serializers.InvenTreeModelSerializer):
level = serializers.IntegerField(read_only=True)
class Meta:
"""Metaclass options."""
model = StockLocation
fields = [
'pk',
@ -519,9 +505,10 @@ class LocationSerializer(InvenTree.serializers.InvenTreeModelSerializer):
class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSerializer):
""" Serializer for StockItemAttachment model """
"""Serializer for StockItemAttachment model."""
def __init__(self, *args, **kwargs):
"""Add detail fields."""
user_detail = kwargs.pop('user_detail', False)
super().__init__(*args, **kwargs)
@ -534,6 +521,8 @@ class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSer
# TODO: Record the uploading user when creating or updating an attachment!
class Meta:
"""Metaclass options."""
model = StockItemAttachment
fields = [
@ -556,7 +545,7 @@ class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSer
class StockItemTestResultSerializer(InvenTree.serializers.InvenTreeModelSerializer):
""" Serializer for the StockItemTestResult model """
"""Serializer for the StockItemTestResult model."""
user_detail = InvenTree.serializers.UserSerializerBrief(source='user', read_only=True)
@ -565,6 +554,7 @@ class StockItemTestResultSerializer(InvenTree.serializers.InvenTreeModelSerializ
attachment = InvenTree.serializers.InvenTreeAttachmentSerializerField(required=False)
def __init__(self, *args, **kwargs):
"""Add detail fields."""
user_detail = kwargs.pop('user_detail', False)
super().__init__(*args, **kwargs)
@ -573,6 +563,8 @@ class StockItemTestResultSerializer(InvenTree.serializers.InvenTreeModelSerializ
self.fields.pop('user_detail')
class Meta:
"""Metaclass options."""
model = StockItemTestResult
fields = [
@ -597,10 +589,10 @@ class StockItemTestResultSerializer(InvenTree.serializers.InvenTreeModelSerializ
class StockTrackingSerializer(InvenTree.serializers.InvenTreeModelSerializer):
""" Serializer for StockItemTracking model """
"""Serializer for StockItemTracking model."""
def __init__(self, *args, **kwargs):
"""Add detail fields."""
item_detail = kwargs.pop('item_detail', False)
user_detail = kwargs.pop('user_detail', False)
@ -621,6 +613,8 @@ class StockTrackingSerializer(InvenTree.serializers.InvenTreeModelSerializer):
deltas = serializers.JSONField(read_only=True)
class Meta:
"""Metaclass options."""
model = StockItemTracking
fields = [
'pk',
@ -644,8 +638,7 @@ class StockTrackingSerializer(InvenTree.serializers.InvenTreeModelSerializer):
class StockAssignmentItemSerializer(serializers.Serializer):
"""
Serializer for a single StockItem with in StockAssignment request.
"""Serializer for a single StockItem with in StockAssignment request.
Here, the particular StockItem is being assigned (manually) to a customer
@ -654,6 +647,8 @@ class StockAssignmentItemSerializer(serializers.Serializer):
"""
class Meta:
"""Metaclass options."""
fields = [
'item',
]
@ -667,7 +662,13 @@ class StockAssignmentItemSerializer(serializers.Serializer):
)
def validate_item(self, item):
"""Validate item.
Ensures:
- is in stock
- Is salable
- Is not allocated
"""
# The item must currently be "in stock"
if not item.in_stock:
raise ValidationError(_("Item must be in stock"))
@ -688,13 +689,14 @@ class StockAssignmentItemSerializer(serializers.Serializer):
class StockAssignmentSerializer(serializers.Serializer):
"""
Serializer for assigning one (or more) stock items to a customer.
"""Serializer for assigning one (or more) stock items to a customer.
This is a manual assignment process, separate for (for example) a Sales Order
"""
class Meta:
"""Metaclass options."""
fields = [
'items',
'customer',
@ -716,7 +718,7 @@ class StockAssignmentSerializer(serializers.Serializer):
)
def validate_customer(self, customer):
"""Make sure provided company is customer."""
if customer and not customer.is_customer:
raise ValidationError(_('Selected company is not a customer'))
@ -730,7 +732,7 @@ class StockAssignmentSerializer(serializers.Serializer):
)
def validate(self, data):
"""Make sure items were provided."""
data = super().validate(data)
items = data.get('items', [])
@ -741,7 +743,7 @@ class StockAssignmentSerializer(serializers.Serializer):
return data
def save(self):
"""Assign stock."""
request = self.context['request']
user = getattr(request, 'user', None)
@ -765,13 +767,14 @@ class StockAssignmentSerializer(serializers.Serializer):
class StockMergeItemSerializer(serializers.Serializer):
"""
Serializer for a single StockItem within the StockMergeSerializer class.
"""Serializer for a single StockItem within the StockMergeSerializer class.
Here, the individual StockItem is being checked for merge compatibility.
"""
class Meta:
"""Metaclass options."""
fields = [
'item',
]
@ -785,7 +788,7 @@ class StockMergeItemSerializer(serializers.Serializer):
)
def validate_item(self, item):
"""Make sure item can be merged."""
# Check that the stock item is able to be merged
item.can_merge(raise_error=True)
@ -793,11 +796,11 @@ class StockMergeItemSerializer(serializers.Serializer):
class StockMergeSerializer(serializers.Serializer):
"""
Serializer for merging two (or more) stock items together
"""
"""Serializer for merging two (or more) stock items together."""
class Meta:
"""Metaclass options."""
fields = [
'items',
'location',
@ -840,7 +843,7 @@ class StockMergeSerializer(serializers.Serializer):
)
def validate(self, data):
"""Make sure all needed values are provided and that the items can be merged."""
data = super().validate(data)
items = data['items']
@ -879,11 +882,10 @@ class StockMergeSerializer(serializers.Serializer):
return data
def save(self):
"""
Actually perform the stock merging action.
"""Actually perform the stock merging action.
At this point we are confident that the merge can take place
"""
data = self.validated_data
base_item = data['base_item']
@ -908,8 +910,7 @@ class StockMergeSerializer(serializers.Serializer):
class StockAdjustmentItemSerializer(serializers.Serializer):
"""
Serializer for a single StockItem within a stock adjument request.
"""Serializer for a single StockItem within a stock adjument request.
Fields:
- item: StockItem object
@ -917,6 +918,8 @@ class StockAdjustmentItemSerializer(serializers.Serializer):
"""
class Meta:
"""Metaclass options."""
fields = [
'item',
'quantity'
@ -940,11 +943,11 @@ class StockAdjustmentItemSerializer(serializers.Serializer):
class StockAdjustmentSerializer(serializers.Serializer):
"""
Base class for managing stock adjustment actions via the API
"""
"""Base class for managing stock adjustment actions via the API."""
class Meta:
"""Metaclass options."""
fields = [
'items',
'notes',
@ -960,7 +963,7 @@ class StockAdjustmentSerializer(serializers.Serializer):
)
def validate(self, data):
"""Make sure items are provided."""
super().validate(data)
items = data.get('items', [])
@ -972,12 +975,10 @@ class StockAdjustmentSerializer(serializers.Serializer):
class StockCountSerializer(StockAdjustmentSerializer):
"""
Serializer for counting stock items
"""
"""Serializer for counting stock items."""
def save(self):
"""Count stock."""
request = self.context['request']
data = self.validated_data
@ -998,12 +999,10 @@ class StockCountSerializer(StockAdjustmentSerializer):
class StockAddSerializer(StockAdjustmentSerializer):
"""
Serializer for adding stock to stock item(s)
"""
"""Serializer for adding stock to stock item(s)."""
def save(self):
"""Add stock."""
request = self.context['request']
data = self.validated_data
@ -1023,12 +1022,10 @@ class StockAddSerializer(StockAdjustmentSerializer):
class StockRemoveSerializer(StockAdjustmentSerializer):
"""
Serializer for removing stock from stock item(s)
"""
"""Serializer for removing stock from stock item(s)."""
def save(self):
"""Remove stock."""
request = self.context['request']
data = self.validated_data
@ -1048,9 +1045,7 @@ class StockRemoveSerializer(StockAdjustmentSerializer):
class StockTransferSerializer(StockAdjustmentSerializer):
"""
Serializer for transferring (moving) stock item(s)
"""
"""Serializer for transferring (moving) stock item(s)."""
location = serializers.PrimaryKeyRelatedField(
queryset=StockLocation.objects.all(),
@ -1062,22 +1057,16 @@ class StockTransferSerializer(StockAdjustmentSerializer):
)
class Meta:
"""Metaclass options."""
fields = [
'items',
'notes',
'location',
]
def validate(self, data):
data = super().validate(data)
# TODO: Any specific validation of location field?
return data
def save(self):
"""Transfer stock."""
request = self.context['request']
data = self.validated_data

View File

@ -1,6 +1,4 @@
"""
Unit testing for the Stock API
"""
"""Unit testing for the Stock API."""
import io
import os
@ -21,6 +19,7 @@ from stock.models import StockItem, StockLocation
class StockAPITestCase(InvenTreeAPITestCase):
"""Mixin for stock api tests."""
fixtures = [
'category',
@ -41,30 +40,28 @@ class StockAPITestCase(InvenTreeAPITestCase):
'stock.delete',
]
def setUp(self):
super().setUp()
class StockLocationTest(StockAPITestCase):
"""
Series of API tests for the StockLocation API
"""
"""Series of API tests for the StockLocation API."""
list_url = reverse('api-location-list')
def setUp(self):
"""Setup for all tests."""
super().setUp()
# Add some stock locations
StockLocation.objects.create(name='top', description='top category')
def test_list(self):
"""Test StockLocation list."""
# Check that we can request the StockLocation list
response = self.client.get(self.list_url, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertGreaterEqual(len(response.data), 1)
def test_add(self):
"""Test adding StockLocation."""
# Check that we can add a new StockLocation
data = {
'parent': 1,
@ -76,17 +73,12 @@ class StockLocationTest(StockAPITestCase):
class StockItemListTest(StockAPITestCase):
"""
Tests for the StockItem API LIST endpoint
"""
"""Tests for the StockItem API LIST endpoint."""
list_url = reverse('api-stock-list')
def get_stock(self, **kwargs):
"""
Filter stock and return JSON object
"""
"""Filter stock and return JSON object."""
response = self.client.get(self.list_url, format='json', data=kwargs)
self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -95,19 +87,13 @@ class StockItemListTest(StockAPITestCase):
return response.data
def test_get_stock_list(self):
"""
List *all* StockItem objects.
"""
"""List *all* StockItem objects."""
response = self.get_stock()
self.assertEqual(len(response), 29)
def test_filter_by_part(self):
"""
Filter StockItem by Part reference
"""
"""Filter StockItem by Part reference."""
response = self.get_stock(part=25)
self.assertEqual(len(response), 17)
@ -116,19 +102,13 @@ class StockItemListTest(StockAPITestCase):
self.assertEqual(len(response), 12)
def test_filter_by_IPN(self):
"""
Filter StockItem by IPN reference
"""
def test_filter_by_ipn(self):
"""Filter StockItem by IPN reference."""
response = self.get_stock(IPN="R.CH")
self.assertEqual(len(response), 3)
def test_filter_by_location(self):
"""
Filter StockItem by StockLocation reference
"""
"""Filter StockItem by StockLocation reference."""
response = self.get_stock(location=5)
self.assertEqual(len(response), 1)
@ -142,10 +122,7 @@ class StockItemListTest(StockAPITestCase):
self.assertEqual(len(response), 18)
def test_filter_by_depleted(self):
"""
Filter StockItem by depleted status
"""
"""Filter StockItem by depleted status."""
response = self.get_stock(depleted=1)
self.assertEqual(len(response), 1)
@ -153,10 +130,7 @@ class StockItemListTest(StockAPITestCase):
self.assertEqual(len(response), 28)
def test_filter_by_in_stock(self):
"""
Filter StockItem by 'in stock' status
"""
"""Filter StockItem by 'in stock' status."""
response = self.get_stock(in_stock=1)
self.assertEqual(len(response), 26)
@ -164,10 +138,7 @@ class StockItemListTest(StockAPITestCase):
self.assertEqual(len(response), 3)
def test_filter_by_status(self):
"""
Filter StockItem by 'status' field
"""
"""Filter StockItem by 'status' field."""
codes = {
StockStatus.OK: 27,
StockStatus.DESTROYED: 1,
@ -183,18 +154,12 @@ class StockItemListTest(StockAPITestCase):
self.assertEqual(len(response), num)
def test_filter_by_batch(self):
"""
Filter StockItem by batch code
"""
"""Filter StockItem by batch code."""
response = self.get_stock(batch='B123')
self.assertEqual(len(response), 1)
def test_filter_by_serialized(self):
"""
Filter StockItem by serialized status
"""
"""Filter StockItem by serialized status."""
response = self.get_stock(serialized=1)
self.assertEqual(len(response), 12)
@ -208,10 +173,7 @@ class StockItemListTest(StockAPITestCase):
self.assertIsNone(item['serial'])
def test_filter_by_has_batch(self):
"""
Test the 'has_batch' filter, which tests if the stock item has been assigned a batch code
"""
"""Test the 'has_batch' filter, which tests if the stock item has been assigned a batch code."""
with_batch = self.get_stock(has_batch=1)
without_batch = self.get_stock(has_batch=0)
@ -227,11 +189,10 @@ class StockItemListTest(StockAPITestCase):
self.assertTrue(item['batch'] in [None, ''])
def test_filter_by_tracked(self):
"""
Test the 'tracked' filter.
"""Test the 'tracked' filter.
This checks if the stock item has either a batch code *or* a serial number
"""
tracked = self.get_stock(tracked=True)
untracked = self.get_stock(tracked=False)
@ -248,10 +209,7 @@ class StockItemListTest(StockAPITestCase):
self.assertTrue(item['batch'] in blank and item['serial'] in blank)
def test_filter_by_expired(self):
"""
Filter StockItem by expiry status
"""
"""Filter StockItem by expiry status."""
# First, we can assume that the 'stock expiry' feature is disabled
response = self.get_stock(expired=1)
self.assertEqual(len(response), 29)
@ -289,10 +247,7 @@ class StockItemListTest(StockAPITestCase):
self.assertEqual(len(response), 25)
def test_paginate(self):
"""
Test that we can paginate results correctly
"""
"""Test that we can paginate results correctly."""
for n in [1, 5, 10]:
response = self.get_stock(limit=n)
@ -302,7 +257,7 @@ class StockItemListTest(StockAPITestCase):
self.assertEqual(len(response['results']), n)
def export_data(self, filters=None):
"""Helper to test exports."""
if not filters:
filters = {}
@ -321,10 +276,7 @@ class StockItemListTest(StockAPITestCase):
return dataset
def test_export(self):
"""
Test exporting of Stock data via the API
"""
"""Test exporting of Stock data via the API."""
dataset = self.export_data({})
# Check that *all* stock item objects have been exported
@ -361,13 +313,12 @@ class StockItemListTest(StockAPITestCase):
class StockItemTest(StockAPITestCase):
"""
Series of API tests for the StockItem API
"""
"""Series of API tests for the StockItem API."""
list_url = reverse('api-stock-list')
def setUp(self):
"""Setup for all tests."""
super().setUp()
# Create some stock locations
top = StockLocation.objects.create(name='A', description='top')
@ -376,11 +327,7 @@ class StockItemTest(StockAPITestCase):
StockLocation.objects.create(name='C', description='location c', parent=top)
def test_create_default_location(self):
"""
Test the default location functionality,
if a 'location' is not specified in the creation request.
"""
"""Test the default location functionality, if a 'location' is not specified in the creation request."""
# The part 'R_4K7_0603' (pk=4) has a default location specified
response = self.client.post(
@ -423,10 +370,7 @@ class StockItemTest(StockAPITestCase):
self.assertEqual(response.data['location'], None)
def test_stock_item_create(self):
"""
Test creation of a StockItem via the API
"""
"""Test creation of a StockItem via the API."""
# POST with an empty part reference
response = self.client.post(
@ -476,10 +420,7 @@ class StockItemTest(StockAPITestCase):
)
def test_creation_with_serials(self):
"""
Test that serialized stock items can be created via the API,
"""
"""Test that serialized stock items can be created via the API."""
trackable_part = part.models.Part.objects.create(
name='My part',
description='A trackable part',
@ -537,8 +478,7 @@ class StockItemTest(StockAPITestCase):
self.assertEqual(trackable_part.get_stock_count(), 10)
def test_default_expiry(self):
"""
Test that the "default_expiry" functionality works via the API.
"""Test that the "default_expiry" functionality works via the API.
- If an expiry_date is specified, use that
- Otherwise, check if the referenced part has a default_expiry defined
@ -547,9 +487,7 @@ class StockItemTest(StockAPITestCase):
Notes:
- Part <25> has a default_expiry of 10 days
"""
# First test - create a new StockItem without an expiry date
data = {
'part': 4,
@ -587,10 +525,7 @@ class StockItemTest(StockAPITestCase):
self.assertEqual(response.data['expiry_date'], expiry.isoformat())
def test_purchase_price(self):
"""
Test that we can correctly read and adjust purchase price information via the API
"""
"""Test that we can correctly read and adjust purchase price information via the API."""
url = reverse('api-stock-detail', kwargs={'pk': 1})
data = self.get(url, expected_code=200).data
@ -648,8 +583,7 @@ class StockItemTest(StockAPITestCase):
self.assertEqual(data['purchase_price_currency'], 'NZD')
def test_install(self):
""" Test that stock item can be installed into antoher item, via the API """
"""Test that stock item can be installed into antoher item, via the API."""
# Select the "parent" stock item
parent_part = part.models.Part.objects.get(pk=100)
@ -731,16 +665,10 @@ class StockItemTest(StockAPITestCase):
class StocktakeTest(StockAPITestCase):
"""
Series of tests for the Stocktake API
"""
"""Series of tests for the Stocktake API."""
def test_action(self):
"""
Test each stocktake action endpoint,
for validation
"""
"""Test each stocktake action endpoint, for validation."""
for endpoint in ['api-stock-count', 'api-stock-add', 'api-stock-remove']:
url = reverse(endpoint)
@ -796,10 +724,7 @@ class StocktakeTest(StockAPITestCase):
self.assertContains(response, 'Ensure this value is greater than or equal to 0', status_code=status.HTTP_400_BAD_REQUEST)
def test_transfer(self):
"""
Test stock transfers
"""
"""Test stock transfers."""
data = {
'items': [
{
@ -825,12 +750,10 @@ class StocktakeTest(StockAPITestCase):
class StockItemDeletionTest(StockAPITestCase):
"""
Tests for stock item deletion via the API
"""
"""Tests for stock item deletion via the API."""
def test_delete(self):
"""Test stock item deletion."""
n = StockItem.objects.count()
# Create and then delete a bunch of stock items
@ -861,12 +784,14 @@ class StockItemDeletionTest(StockAPITestCase):
class StockTestResultTest(StockAPITestCase):
"""Tests for StockTestResult APIs."""
def get_url(self):
"""Helper funtion to get test-result api url."""
return reverse('api-stock-test-result-list')
def test_list(self):
"""Test list endpoint."""
url = self.get_url()
response = self.client.get(url)
@ -878,6 +803,7 @@ class StockTestResultTest(StockAPITestCase):
self.assertGreaterEqual(len(response.data), 4)
def test_post_fail(self):
"""Test failing posts."""
# Attempt to post a new test result without specifying required data
url = self.get_url()
@ -907,8 +833,7 @@ class StockTestResultTest(StockAPITestCase):
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def test_post(self):
# Test creation of a new test result
"""Test creation of a new test result."""
url = self.get_url()
response = self.client.get(url)
@ -939,8 +864,7 @@ class StockTestResultTest(StockAPITestCase):
self.assertEqual(test['user'], self.user.pk)
def test_post_bitmap(self):
"""
2021-08-25
"""2021-08-25.
For some (unknown) reason, prior to fix https://github.com/inventree/InvenTree/pull/2018
uploading a bitmap image would result in a failure.
@ -949,7 +873,6 @@ class StockTestResultTest(StockAPITestCase):
As a bonus this also tests the file-upload component
"""
here = os.path.dirname(__file__)
image_file = os.path.join(here, 'fixtures', 'test_image.bmp')
@ -974,15 +897,12 @@ class StockTestResultTest(StockAPITestCase):
class StockAssignTest(StockAPITestCase):
"""
Unit tests for the stock assignment API endpoint,
where stock items are manually assigned to a customer
"""
"""Unit tests for the stock assignment API endpoint, where stock items are manually assigned to a customer."""
URL = reverse('api-stock-assign')
def test_invalid(self):
"""Test invalid assign."""
# Test with empty data
response = self.post(
self.URL,
@ -1049,7 +969,7 @@ class StockAssignTest(StockAPITestCase):
self.assertIn('Item must be in stock', str(response.data['items'][0]))
def test_valid(self):
"""Test valid assign."""
stock_items = []
for i in range(5):
@ -1083,14 +1003,12 @@ class StockAssignTest(StockAPITestCase):
class StockMergeTest(StockAPITestCase):
"""
Unit tests for merging stock items via the API
"""
"""Unit tests for merging stock items via the API."""
URL = reverse('api-stock-merge')
def setUp(self):
"""Setup for all tests."""
super().setUp()
self.part = part.models.Part.objects.get(pk=25)
@ -1117,10 +1035,7 @@ class StockMergeTest(StockAPITestCase):
)
def test_missing_data(self):
"""
Test responses which are missing required data
"""
"""Test responses which are missing required data."""
# Post completely empty
data = self.post(
@ -1145,10 +1060,7 @@ class StockMergeTest(StockAPITestCase):
self.assertIn('At least two stock items', str(data))
def test_invalid_data(self):
"""
Test responses which have invalid data
"""
"""Test responses which have invalid data."""
# Serialized stock items should be rejected
data = self.post(
self.URL,
@ -1229,10 +1141,7 @@ class StockMergeTest(StockAPITestCase):
self.assertIn('Stock items must refer to the same supplier part', str(data))
def test_valid_merge(self):
"""
Test valid merging of stock items
"""
"""Test valid merging of stock items."""
# Check initial conditions
n = StockItem.objects.filter(part=self.part).count()
self.assertEqual(self.item_1.quantity, 100)

View File

@ -1,4 +1,4 @@
""" Unit tests for Stock views (see views.py) """
"""Unit tests for Stock views (see views.py)."""
from django.urls import reverse
@ -8,6 +8,7 @@ from InvenTree.helpers import InvenTreeTestCase
class StockViewTestCase(InvenTreeTestCase):
"""Mixin for Stockview tests."""
fixtures = [
'category',
@ -22,18 +23,19 @@ class StockViewTestCase(InvenTreeTestCase):
class StockListTest(StockViewTestCase):
""" Tests for Stock list views """
"""Tests for Stock list views."""
def test_stock_index(self):
"""Test stock index page."""
response = self.client.get(reverse('stock-index'))
self.assertEqual(response.status_code, 200)
class StockOwnershipTest(StockViewTestCase):
""" Tests for stock ownership views """
"""Tests for stock ownership views."""
def setUp(self):
""" Add another user for ownership tests """
"""Add another user for ownership tests."""
"""
TODO: Refactor this following test to use the new API form

View File

@ -1,3 +1,5 @@
"""Tests for stock app."""
import datetime
from django.core.exceptions import ValidationError
@ -13,9 +15,7 @@ from .models import (StockItem, StockItemTestResult, StockItemTracking,
class StockTest(InvenTreeTestCase):
"""
Tests to ensure that the stock location tree functions correcly
"""
"""Tests to ensure that the stock location tree functions correcly."""
fixtures = [
'category',
@ -27,6 +27,7 @@ class StockTest(InvenTreeTestCase):
]
def setUp(self):
"""Setup for all tests."""
super().setUp()
# Extract some shortcuts from the fixtures
@ -44,10 +45,7 @@ class StockTest(InvenTreeTestCase):
StockItem.objects.rebuild()
def test_expiry(self):
"""
Test expiry date functionality for StockItem model.
"""
"""Test expiry date functionality for StockItem model."""
today = datetime.datetime.now().date()
item = StockItem.objects.create(
@ -78,10 +76,7 @@ class StockTest(InvenTreeTestCase):
self.assertTrue(item.is_expired())
def test_is_building(self):
"""
Test that the is_building flag does not count towards stock.
"""
"""Test that the is_building flag does not count towards stock."""
part = Part.objects.get(pk=1)
# Record the total stock count
@ -107,25 +102,29 @@ class StockTest(InvenTreeTestCase):
self.assertEqual(part.quantity_being_built, 1)
def test_loc_count(self):
"""Test count function."""
self.assertEqual(StockLocation.objects.count(), 7)
def test_url(self):
"""Test get_absolute_url function."""
it = StockItem.objects.get(pk=2)
self.assertEqual(it.get_absolute_url(), '/stock/item/2/')
self.assertEqual(self.home.get_absolute_url(), '/stock/location/1/')
def test_barcode(self):
"""Test format_barcode."""
barcode = self.office.format_barcode(brief=False)
self.assertIn('"name": "Office"', barcode)
def test_strings(self):
"""Test str function."""
it = StockItem.objects.get(pk=1)
self.assertEqual(str(it), '4000 x M2x4 LPHS @ Dining Room')
def test_parent_locations(self):
"""Test parent."""
self.assertEqual(self.office.parent, None)
self.assertEqual(self.drawer1.parent, self.office)
self.assertEqual(self.drawer2.parent, self.office)
@ -142,6 +141,7 @@ class StockTest(InvenTreeTestCase):
self.assertEqual(self.drawer3.pathstring, 'Home/Drawer_3')
def test_children(self):
"""Test has_children."""
self.assertTrue(self.office.has_children)
self.assertFalse(self.drawer2.has_children)
@ -154,15 +154,14 @@ class StockTest(InvenTreeTestCase):
self.assertNotIn(self.bathroom.id, childs)
def test_items(self):
self.assertTrue(self.drawer1.has_items())
self.assertTrue(self.drawer3.has_items())
self.assertFalse(self.drawer2.has_items())
"""Test has_items."""
# Drawer 3 should have three stock items
self.assertEqual(self.drawer3.stock_items.count(), 18)
self.assertEqual(self.drawer3.item_count, 18)
def test_stock_count(self):
"""Test stock count."""
part = Part.objects.get(pk=1)
entries = part.stock_entries()
@ -177,7 +176,7 @@ class StockTest(InvenTreeTestCase):
)
def test_delete_location(self):
"""Test deleting stock."""
# How many stock items are there?
n_stock = StockItem.objects.count()
@ -196,8 +195,7 @@ class StockTest(InvenTreeTestCase):
self.assertEqual(s_item.location, self.office)
def test_move(self):
""" Test stock movement functions """
"""Test stock movement functions."""
# Move 4,000 screws to the bathroom
it = StockItem.objects.get(pk=1)
self.assertNotEqual(it.location, self.bathroom)
@ -215,6 +213,7 @@ class StockTest(InvenTreeTestCase):
self.assertEqual(track.notes, 'Moved to the bathroom')
def test_self_move(self):
"""Test moving stock to itself does not work."""
# Try to move an item to its current location (should fail)
it = StockItem.objects.get(pk=1)
@ -225,6 +224,7 @@ class StockTest(InvenTreeTestCase):
self.assertEqual(it.tracking_info.count(), n)
def test_partial_move(self):
"""Test partial stock moving."""
w1 = StockItem.objects.get(pk=100)
# A batch code is required to split partial stock!
@ -249,6 +249,7 @@ class StockTest(InvenTreeTestCase):
self.assertFalse(widget.move(None, 'null', None))
def test_split_stock(self):
"""Test stock splitting."""
# Split the 1234 x 2K2 resistors in Drawer_1
n = StockItem.objects.filter(part=3).count()
@ -268,6 +269,7 @@ class StockTest(InvenTreeTestCase):
self.assertEqual(StockItem.objects.filter(part=3).count(), n + 1)
def test_stocktake(self):
"""Test stocktake function."""
# Perform stocktake
it = StockItem.objects.get(pk=2)
self.assertEqual(it.quantity, 5000)
@ -288,6 +290,7 @@ class StockTest(InvenTreeTestCase):
self.assertEqual(it.tracking_info.count(), n)
def test_add_stock(self):
"""Test adding stock."""
it = StockItem.objects.get(pk=2)
n = it.quantity
it.add_stock(45, None, notes='Added some items')
@ -303,6 +306,7 @@ class StockTest(InvenTreeTestCase):
self.assertFalse(it.add_stock(-10, None))
def test_take_stock(self):
"""Test stock removal."""
it = StockItem.objects.get(pk=2)
n = it.quantity
it.take_stock(15, None, notes='Removed some items')
@ -320,7 +324,7 @@ class StockTest(InvenTreeTestCase):
self.assertFalse(it.take_stock(-10, None))
def test_deplete_stock(self):
"""Test depleted stock deletion."""
w1 = StockItem.objects.get(pk=100)
w2 = StockItem.objects.get(pk=101)
@ -339,10 +343,7 @@ class StockTest(InvenTreeTestCase):
w2 = StockItem.objects.get(pk=101)
def test_serials(self):
"""
Tests for stock serialization
"""
"""Tests for stock serialization."""
p = Part.objects.create(
name='trackable part',
description='trackable part',
@ -373,10 +374,7 @@ class StockTest(InvenTreeTestCase):
self.assertTrue(item.serialized)
def test_big_serials(self):
"""
Unit tests for "large" serial numbers which exceed integer encoding
"""
"""Unit tests for "large" serial numbers which exceed integer encoding."""
p = Part.objects.create(
name='trackable part',
description='trackable part',
@ -451,11 +449,10 @@ class StockTest(InvenTreeTestCase):
self.assertEqual(item_prev.serial_int, 99)
def test_serialize_stock_invalid(self):
"""
Test manual serialization of parts.
"""Test manual serialization of parts.
Each of these tests should fail
"""
# Test serialization of non-serializable part
item = StockItem.objects.get(pk=1234)
@ -480,8 +477,7 @@ class StockTest(InvenTreeTestCase):
item.serializeStock(3, "hello", self.user)
def test_serialize_stock_valid(self):
""" Perform valid stock serializations """
"""Perform valid stock serializations."""
# There are 10 of these in stock
# Item will deplete when deleted
item = StockItem.objects.get(pk=100)
@ -517,15 +513,14 @@ class StockTest(InvenTreeTestCase):
item.serializeStock(2, [99, 100], self.user)
def test_location_tree(self):
"""
Unit tests for stock location tree structure (MPTT).
"""Unit tests for stock location tree structure (MPTT).
Ensure that the MPTT structure is rebuilt correctly,
and the corrent ancestor tree is observed.
Ref: https://github.com/inventree/InvenTree/issues/2636
Ref: https://github.com/inventree/InvenTree/issues/2733
"""
# First, we will create a stock location structure
A = StockLocation.objects.create(
@ -686,11 +681,10 @@ class StockTest(InvenTreeTestCase):
class VariantTest(StockTest):
"""
Tests for calculation stock counts against templates / variants
"""
"""Tests for calculation stock counts against templates / variants."""
def test_variant_stock(self):
"""Test variant functions."""
# Check the 'Chair' variant
chair = Part.objects.get(pk=10000)
@ -704,8 +698,7 @@ class VariantTest(StockTest):
self.assertEqual(green.stock_entries().count(), 3)
def test_serial_numbers(self):
# Test serial number functionality for variant / template parts
"""Test serial number functionality for variant / template parts."""
chair = Part.objects.get(pk=10000)
# Operations on the top-level object
@ -769,11 +762,10 @@ class VariantTest(StockTest):
class TestResultTest(StockTest):
"""
Tests for the StockItemTestResult model.
"""
"""Tests for the StockItemTestResult model."""
def test_test_count(self):
"""Test test count."""
item = StockItem.objects.get(pk=105)
tests = item.test_results
self.assertEqual(tests.count(), 4)
@ -795,7 +787,7 @@ class TestResultTest(StockTest):
self.assertIn(test, result_map.keys())
def test_test_results(self):
"""Test test results."""
item = StockItem.objects.get(pk=522)
status = item.requiredTestStatus()
@ -832,7 +824,7 @@ class TestResultTest(StockTest):
self.assertTrue(item.passedAllRequiredTests())
def test_duplicate_item_tests(self):
"""Test duplicate item behaviour."""
# Create an example stock item by copying one from the database (because we are lazy)
item = StockItem.objects.get(pk=522)
@ -898,12 +890,10 @@ class TestResultTest(StockTest):
self.assertEqual(item3.test_results.count(), 4)
def test_installed_tests(self):
"""
Test test results for stock in stock.
"""Test test results for stock in stock.
Or, test "test results" for "stock items" installed "inside" a "stock item"
"""
# Get a "master" stock item
item = StockItem.objects.get(pk=105)

View File

@ -1,6 +1,4 @@
"""
URL lookup for Stock app
"""
"""URL lookup for Stock app."""
from django.urls import include, re_path

View File

@ -1,6 +1,4 @@
"""
Django views for interacting with Stock app
"""
"""Django views for interacting with Stock app."""
from datetime import datetime
@ -21,13 +19,14 @@ from .models import StockItem, StockItemTracking, StockLocation
class StockIndex(InvenTreeRoleMixin, InvenTreePluginViewMixin, ListView):
""" StockIndex view loads all StockLocation and StockItem object
"""
"""StockIndex view loads all StockLocation and StockItem object."""
model = StockItem
template_name = 'stock/location.html'
context_obect_name = 'locations'
def get_context_data(self, **kwargs):
"""Extend template context."""
context = super().get_context_data(**kwargs).copy()
# Return all top-level locations
@ -48,9 +47,7 @@ class StockIndex(InvenTreeRoleMixin, InvenTreePluginViewMixin, ListView):
class StockLocationDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
"""
Detailed view of a single StockLocation object
"""
"""Detailed view of a single StockLocation object."""
context_object_name = 'location'
template_name = 'stock/location.html'
@ -58,7 +55,7 @@ class StockLocationDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailVi
model = StockLocation
def get_context_data(self, **kwargs):
"""Extend template context."""
context = super().get_context_data(**kwargs)
context['ownership_enabled'] = common.models.InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL')
@ -69,9 +66,7 @@ class StockLocationDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailVi
class StockItemDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
"""
Detailed view of a single StockItem object
"""
"""Detailed view of a single StockItem object."""
context_object_name = 'item'
template_name = 'stock/item.html'
@ -79,11 +74,7 @@ class StockItemDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
model = StockItem
def get_context_data(self, **kwargs):
"""
Add information on the "next" and "previous" StockItem objects,
based on the serial numbers.
"""
"""Add information on the "next" and "previous" StockItem objects, based on the serial numbers."""
data = super().get_context_data(**kwargs)
if self.object.serialized:
@ -103,8 +94,7 @@ class StockItemDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
return data
def get(self, request, *args, **kwargs):
""" check if item exists else return to stock index """
"""Check if item exists else return to stock index."""
stock_pk = kwargs.get('pk', None)
if stock_pk:
@ -120,14 +110,14 @@ class StockItemDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
class StockLocationQRCode(QRCodeView):
""" View for displaying a QR code for a StockLocation object """
"""View for displaying a QR code for a StockLocation object."""
ajax_form_title = _("Stock Location QR code")
role_required = ['stock_location.view', 'stock.view']
def get_qr_data(self):
""" Generate QR code data for the StockLocation """
"""Generate QR code data for the StockLocation."""
try:
loc = StockLocation.objects.get(id=self.pk)
return loc.format_barcode()
@ -136,9 +126,7 @@ class StockLocationQRCode(QRCodeView):
class StockItemReturnToStock(AjaxUpdateView):
"""
View for returning a stock item (which is assigned to a customer) to stock.
"""
"""View for returning a stock item (which is assigned to a customer) to stock."""
model = StockItem
ajax_form_title = _("Return to Stock")
@ -146,29 +134,28 @@ class StockItemReturnToStock(AjaxUpdateView):
form_class = StockForms.ReturnStockItemForm
def validate(self, item, form, **kwargs):
"""Make sure required data is there."""
location = form.cleaned_data.get('location', None)
if not location:
form.add_error('location', _('Specify a valid location'))
def save(self, item, form, **kwargs):
"""Return stock."""
location = form.cleaned_data.get('location', None)
if location:
item.returnFromCustomer(location, self.request.user)
def get_data(self):
"""Set success message."""
return {
'success': _('Stock item returned from customer')
}
class StockItemDeleteTestData(AjaxUpdateView):
"""
View for deleting all test data
"""
"""View for deleting all test data."""
model = StockItem
form_class = ConfirmForm
@ -177,10 +164,11 @@ class StockItemDeleteTestData(AjaxUpdateView):
role_required = ['stock.change', 'stock.delete']
def get_form(self):
"""Require confirm."""
return ConfirmForm()
def post(self, request, *args, **kwargs):
"""Delete test data."""
valid = False
stock_item = StockItem.objects.get(pk=self.kwargs['pk'])
@ -203,13 +191,13 @@ class StockItemDeleteTestData(AjaxUpdateView):
class StockItemQRCode(QRCodeView):
""" View for displaying a QR code for a StockItem object """
"""View for displaying a QR code for a StockItem object."""
ajax_form_title = _("Stock Item QR Code")
role_required = 'stock.view'
def get_qr_data(self):
""" Generate QR code data for the StockItem """
"""Generate QR code data for the StockItem."""
try:
item = StockItem.objects.get(id=self.pk)
return item.format_barcode()
@ -218,9 +206,7 @@ class StockItemQRCode(QRCodeView):
class StockItemConvert(AjaxUpdateView):
"""
View for 'converting' a StockItem to a variant of its current part.
"""
"""View for 'converting' a StockItem to a variant of its current part."""
model = StockItem
form_class = StockForms.ConvertStockItemForm
@ -229,10 +215,7 @@ class StockItemConvert(AjaxUpdateView):
context_object_name = 'item'
def get_form(self):
"""
Filter the available parts.
"""
"""Filter the available parts."""
form = super().get_form()
item = self.get_object()
@ -241,7 +224,7 @@ class StockItemConvert(AjaxUpdateView):
return form
def save(self, obj, form):
"""Convert item to variant."""
stock_item = self.get_object()
variant = form.cleaned_data.get('part', None)
@ -252,8 +235,8 @@ class StockItemConvert(AjaxUpdateView):
class StockLocationDelete(AjaxDeleteView):
"""
View to delete a StockLocation
"""View to delete a StockLocation.
Presents a deletion confirmation form to the user
"""
@ -265,8 +248,8 @@ class StockLocationDelete(AjaxDeleteView):
class StockItemDelete(AjaxDeleteView):
"""
View to delete a StockItem
"""View to delete a StockItem.
Presents a deletion confirmation form to the user
"""
@ -278,8 +261,8 @@ class StockItemDelete(AjaxDeleteView):
class StockItemTrackingDelete(AjaxDeleteView):
"""
View to delete a StockItemTracking object
"""View to delete a StockItemTracking object.
Presents a deletion confirmation form to the user
"""
@ -289,7 +272,7 @@ class StockItemTrackingDelete(AjaxDeleteView):
class StockItemTrackingEdit(AjaxUpdateView):
""" View for editing a StockItemTracking object """
"""View for editing a StockItemTracking object."""
model = StockItemTracking
ajax_form_title = _('Edit Stock Tracking Entry')
@ -297,15 +280,14 @@ class StockItemTrackingEdit(AjaxUpdateView):
class StockItemTrackingCreate(AjaxCreateView):
""" View for creating a new StockItemTracking object.
"""
"""View for creating a new StockItemTracking object."""
model = StockItemTracking
ajax_form_title = _("Add Stock Tracking Entry")
form_class = StockForms.TrackingEntryForm
def post(self, request, *args, **kwargs):
"""Create StockItemTracking object."""
self.request = request
self.form = self.get_form()