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 merge99676ee
* 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 commitca96654622
. * use typing here * Revert "Make API code cleaner" This reverts commit24fb68bd3e
. * 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:
@ -1,5 +1,4 @@
|
||||
"""
|
||||
The Stock module is responsible for Stock management.
|
||||
"""The Stock module is responsible for Stock management.
|
||||
|
||||
It includes models for:
|
||||
|
||||
|
@ -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')
|
||||
|
||||
|
@ -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
|
||||
|
@ -1,6 +1,8 @@
|
||||
"""AppConfig for stock app."""
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class StockConfig(AppConfig):
|
||||
"""AppConfig for stock app."""
|
||||
name = 'stock'
|
||||
|
@ -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
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
URL lookup for Stock app
|
||||
"""
|
||||
"""URL lookup for Stock app."""
|
||||
|
||||
from django.urls import include, re_path
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
Reference in New Issue
Block a user