reformat comments 1

This commit is contained in:
Matthias 2022-05-28 02:04:02 +02:00
parent 959e4bb28e
commit 9b40fddf7c
No known key found for this signature in database
GPG Key ID: AB6D0E6C4CB65093
22 changed files with 262 additions and 682 deletions

View File

@ -567,8 +567,7 @@ def rename_asset(instance, filename):
class ReportAsset(models.Model): class ReportAsset(models.Model):
""" """Asset file for use in report templates.
Asset file for use in report templates.
For example, an image to use in a header file. For example, an image to use in a header file.
Uploaded asset files appear in MEDIA_ROOT/report/assets, Uploaded asset files appear in MEDIA_ROOT/report/assets,
and can be loaded in a template using the {% report_asset <filename> %} tag. and can be loaded in a template using the {% report_asset <filename> %} tag.

View File

@ -1,6 +1,4 @@
""" """Template tags for rendering various barcodes"""
Template tags for rendering various barcodes
"""
import base64 import base64
from io import BytesIO from io import BytesIO
@ -14,12 +12,10 @@ register = template.Library()
def image_data(img, fmt='PNG'): def image_data(img, fmt='PNG'):
""" """Convert an image into HTML renderable data
Convert an image into HTML renderable data
Returns a string ``data:image/FMT;base64,xxxxxxxxx`` which can be rendered to an <img> tag Returns a string ``data:image/FMT;base64,xxxxxxxxx`` which can be rendered to an <img> tag
""" """
buffered = BytesIO() buffered = BytesIO()
img.save(buffered, format=fmt) img.save(buffered, format=fmt)
@ -30,8 +26,7 @@ def image_data(img, fmt='PNG'):
@register.simple_tag() @register.simple_tag()
def qrcode(data, **kwargs): def qrcode(data, **kwargs):
""" """Return a byte-encoded QR code image
Return a byte-encoded QR code image
Optional kwargs Optional kwargs
--------------- ---------------
@ -39,7 +34,6 @@ def qrcode(data, **kwargs):
fill_color: Fill color (default = black) fill_color: Fill color (default = black)
back_color: Background color (default = white) back_color: Background color (default = white)
""" """
# Construct "default" values # Construct "default" values
params = dict( params = dict(
box_size=20, box_size=20,
@ -63,10 +57,7 @@ def qrcode(data, **kwargs):
@register.simple_tag() @register.simple_tag()
def barcode(data, barcode_class='code128', **kwargs): def barcode(data, barcode_class='code128', **kwargs):
""" """Render a barcode"""
Render a barcode
"""
constructor = python_barcode.get_barcode_class(barcode_class) constructor = python_barcode.get_barcode_class(barcode_class)
data = str(data).zfill(constructor.digits) data = str(data).zfill(constructor.digits)

View File

@ -1,6 +1,4 @@
""" """Custom template tags for report generation"""
Custom template tags for report generation
"""
import os import os
@ -19,10 +17,7 @@ register = template.Library()
@register.simple_tag() @register.simple_tag()
def asset(filename): def asset(filename):
""" """Return fully-qualified path for an upload report asset file."""
Return fully-qualified path for an upload report asset file.
"""
# If in debug mode, return URL to the image, not a local file # If in debug mode, return URL to the image, not a local file
debug_mode = InvenTreeSetting.get_setting('REPORT_DEBUG_MODE') debug_mode = InvenTreeSetting.get_setting('REPORT_DEBUG_MODE')
@ -38,10 +33,7 @@ def asset(filename):
@register.simple_tag() @register.simple_tag()
def part_image(part): def part_image(part):
""" """Return a fully-qualified path for a part image"""
Return a fully-qualified path for a part image
"""
# If in debug mode, return URL to the image, not a local file # If in debug mode, return URL to the image, not a local file
debug_mode = InvenTreeSetting.get_setting('REPORT_DEBUG_MODE') debug_mode = InvenTreeSetting.get_setting('REPORT_DEBUG_MODE')
@ -75,10 +67,7 @@ def part_image(part):
@register.simple_tag() @register.simple_tag()
def company_image(company): def company_image(company):
""" """Return a fully-qualified path for a company image"""
Return a fully-qualified path for a company image
"""
# If in debug mode, return the URL to the image, not a local file # If in debug mode, return the URL to the image, not a local file
debug_mode = InvenTreeSetting.get_setting('REPORT_DEBUG_MODE') debug_mode = InvenTreeSetting.get_setting('REPORT_DEBUG_MODE')
@ -108,15 +97,13 @@ def company_image(company):
@register.simple_tag() @register.simple_tag()
def internal_link(link, text): def internal_link(link, text):
""" """Make a <a></a> href which points to an InvenTree URL.
Make a <a></a> href which points to an InvenTree URL.
Important Note: This only works if the INVENTREE_BASE_URL parameter is set! Important Note: This only works if the INVENTREE_BASE_URL parameter is set!
If the INVENTREE_BASE_URL parameter is not configured, If the INVENTREE_BASE_URL parameter is not configured,
the text will be returned (unlinked) the text will be returned (unlinked)
""" """
text = str(text) text = str(text)
url = InvenTree.helpers.construct_absolute_url(link) url = InvenTree.helpers.construct_absolute_url(link)

View File

@ -36,10 +36,7 @@ class ReportTest(InvenTreeAPITestCase):
super().setUp() super().setUp()
def copyReportTemplate(self, filename, description): def copyReportTemplate(self, filename, description):
""" """Copy the provided report template into the required media directory"""
Copy the provided report template into the required media directory
"""
src_dir = os.path.join( src_dir = os.path.join(
os.path.dirname(os.path.realpath(__file__)), os.path.dirname(os.path.realpath(__file__)),
'templates', 'templates',
@ -81,10 +78,7 @@ class ReportTest(InvenTreeAPITestCase):
) )
def test_list_endpoint(self): def test_list_endpoint(self):
""" """Test that the LIST endpoint works for each report"""
Test that the LIST endpoint works for each report
"""
if not self.list_url: if not self.list_url:
return return
@ -135,10 +129,7 @@ class TestReportTest(ReportTest):
return super().setUp() return super().setUp()
def test_print(self): def test_print(self):
""" """Printing tests for the TestReport"""
Printing tests for the TestReport
"""
report = self.model.objects.first() report = self.model.objects.first()
url = reverse(self.print_url, kwargs={'pk': report.pk}) url = reverse(self.print_url, kwargs={'pk': report.pk})
@ -177,10 +168,7 @@ class BuildReportTest(ReportTest):
return super().setUp() return super().setUp()
def test_print(self): def test_print(self):
""" """Printing tests for the BuildReport"""
Printing tests for the BuildReport
"""
report = self.model.objects.first() report = self.model.objects.first()
url = reverse(self.print_url, kwargs={'pk': report.pk}) url = reverse(self.print_url, kwargs={'pk': report.pk})

View File

@ -1,6 +1,4 @@
""" """This script calculates translation coverage for various languages"""
This script calculates translation coverage for various languages
"""
import json import json
import os import os

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: It includes models for:

View File

@ -15,7 +15,7 @@ from .models import (StockItem, StockItemAttachment, StockItemTestResult,
class LocationResource(ModelResource): 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 = Field(attribute='parent', widget=widgets.ForeignKeyWidget(StockLocation))
@ -42,9 +42,7 @@ class LocationResource(ModelResource):
class LocationInline(admin.TabularInline): class LocationInline(admin.TabularInline):
""" """Inline for sub-locations"""
Inline for sub-locations
"""
model = StockLocation model = StockLocation
@ -66,7 +64,7 @@ class LocationAdmin(ImportExportModelAdmin):
class StockItemResource(ModelResource): class StockItemResource(ModelResource):
""" Class for managing StockItem data import/export """ """Class for managing StockItem data import/export"""
# Custom managers for ForeignKey fields # Custom managers for ForeignKey fields
part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part)) part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part))

View File

@ -1,6 +1,4 @@
""" """JSON API for the Stock app"""
JSON API for the Stock app
"""
from collections import OrderedDict from collections import OrderedDict
from datetime import datetime, timedelta from datetime import datetime, timedelta
@ -39,7 +37,7 @@ from stock.models import (StockItem, StockItemAttachment, StockItemTestResult,
class StockDetail(generics.RetrieveUpdateDestroyAPIView): class StockDetail(generics.RetrieveUpdateDestroyAPIView):
""" API detail endpoint for Stock object """API detail endpoint for Stock object
get: get:
Return a single StockItem object Return a single StockItem object
@ -89,7 +87,7 @@ class StockMetadata(generics.RetrieveUpdateAPIView):
class StockItemContextMixin: 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): def get_serializer_context(self):
@ -105,17 +103,14 @@ class StockItemContextMixin:
class StockItemSerialize(StockItemContextMixin, generics.CreateAPIView): class StockItemSerialize(StockItemContextMixin, generics.CreateAPIView):
""" """API endpoint for serializing a stock item"""
API endpoint for serializing a stock item
"""
queryset = StockItem.objects.none() queryset = StockItem.objects.none()
serializer_class = StockSerializers.SerializeStockItemSerializer serializer_class = StockSerializers.SerializeStockItemSerializer
class StockItemInstall(StockItemContextMixin, generics.CreateAPIView): 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.part must be in the BOM for this part
- stock_item must currently be "in stock" - stock_item must currently be "in stock"
@ -127,17 +122,14 @@ class StockItemInstall(StockItemContextMixin, generics.CreateAPIView):
class StockItemUninstall(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() queryset = StockItem.objects.none()
serializer_class = StockSerializers.UninstallStockItemSerializer serializer_class = StockSerializers.UninstallStockItemSerializer
class StockAdjustView(generics.CreateAPIView): class StockAdjustView(generics.CreateAPIView):
""" """A generic class for handling stocktake actions.
A generic class for handling stocktake actions.
Subclasses exist for: Subclasses exist for:
@ -159,41 +151,31 @@ class StockAdjustView(generics.CreateAPIView):
class StockCount(StockAdjustView): class StockCount(StockAdjustView):
""" """Endpoint for counting stock (performing a stocktake)."""
Endpoint for counting stock (performing a stocktake).
"""
serializer_class = StockSerializers.StockCountSerializer serializer_class = StockSerializers.StockCountSerializer
class StockAdd(StockAdjustView): 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 serializer_class = StockSerializers.StockAddSerializer
class StockRemove(StockAdjustView): 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 serializer_class = StockSerializers.StockRemoveSerializer
class StockTransfer(StockAdjustView): class StockTransfer(StockAdjustView):
""" """API endpoint for performing stock movements"""
API endpoint for performing stock movements
"""
serializer_class = StockSerializers.StockTransferSerializer serializer_class = StockSerializers.StockTransferSerializer
class StockAssign(generics.CreateAPIView): 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() queryset = StockItem.objects.all()
serializer_class = StockSerializers.StockAssignmentSerializer serializer_class = StockSerializers.StockAssignmentSerializer
@ -208,9 +190,7 @@ class StockAssign(generics.CreateAPIView):
class StockMerge(generics.CreateAPIView): class StockMerge(generics.CreateAPIView):
""" """API endpoint for merging multiple stock items"""
API endpoint for merging multiple stock items
"""
queryset = StockItem.objects.none() queryset = StockItem.objects.none()
serializer_class = StockSerializers.StockMergeSerializer serializer_class = StockSerializers.StockMergeSerializer
@ -222,8 +202,7 @@ class StockMerge(generics.CreateAPIView):
class StockLocationList(generics.ListCreateAPIView): 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 - GET: Return list of StockLocation objects
- POST: Create a new StockLocation - POST: Create a new StockLocation
@ -233,11 +212,9 @@ class StockLocationList(generics.ListCreateAPIView):
serializer_class = StockSerializers.LocationSerializer serializer_class = StockSerializers.LocationSerializer
def filter_queryset(self, queryset): def filter_queryset(self, queryset):
""" """Custom filtering:
Custom filtering:
- Allow filtering by "null" parent to retrieve top-level stock locations - Allow filtering by "null" parent to retrieve top-level stock locations
""" """
queryset = super().filter_queryset(queryset) queryset = super().filter_queryset(queryset)
params = self.request.query_params params = self.request.query_params
@ -319,10 +296,7 @@ class StockLocationList(generics.ListCreateAPIView):
class StockLocationTree(generics.ListAPIView): 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() queryset = StockLocation.objects.all()
serializer_class = StockSerializers.LocationTreeSerializer serializer_class = StockSerializers.LocationTreeSerializer
@ -337,9 +311,7 @@ class StockLocationTree(generics.ListAPIView):
class StockFilter(rest_filters.FilterSet): class StockFilter(rest_filters.FilterSet):
""" """FilterSet for StockItem LIST API"""
FilterSet for StockItem LIST API
"""
# Part name filters # Part name filters
name = rest_filters.CharFilter(label='Part name (case insensitive)', field_name='part__name', lookup_expr='iexact') name = rest_filters.CharFilter(label='Part name (case insensitive)', field_name='part__name', lookup_expr='iexact')
@ -372,12 +344,10 @@ class StockFilter(rest_filters.FilterSet):
available = rest_filters.BooleanFilter(label='Available', method='filter_available') available = rest_filters.BooleanFilter(label='Available', method='filter_available')
def filter_available(self, queryset, name, value): 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 Here, "available" means that the allocated quantity is less than the total quantity
""" """
if str2bool(value): if str2bool(value):
# The 'quantity' field is greater than the calculated 'allocated' field # The 'quantity' field is greater than the calculated 'allocated' field
queryset = queryset.filter(Q(quantity__gt=F('allocated'))) queryset = queryset.filter(Q(quantity__gt=F('allocated')))
@ -401,10 +371,7 @@ class StockFilter(rest_filters.FilterSet):
serialized = rest_filters.BooleanFilter(label='Has serial number', method='filter_serialized') serialized = rest_filters.BooleanFilter(label='Has serial number', method='filter_serialized')
def filter_serialized(self, queryset, name, value): 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='') q = Q(serial=None) | Q(serial='')
if str2bool(value): if str2bool(value):
@ -417,10 +384,7 @@ class StockFilter(rest_filters.FilterSet):
has_batch = rest_filters.BooleanFilter(label='Has batch code', method='filter_has_batch') has_batch = rest_filters.BooleanFilter(label='Has batch code', method='filter_has_batch')
def filter_has_batch(self, queryset, name, value): 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='') q = Q(batch=None) | Q(batch='')
if str2bool(value): if str2bool(value):
@ -433,12 +397,10 @@ class StockFilter(rest_filters.FilterSet):
tracked = rest_filters.BooleanFilter(label='Tracked', method='filter_tracked') tracked = rest_filters.BooleanFilter(label='Tracked', method='filter_tracked')
def filter_tracked(self, queryset, name, value): 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 serial number
- It has a batch code - It has a batch code
""" """
q_batch = Q(batch=None) | Q(batch='') q_batch = Q(batch=None) | Q(batch='')
q_serial = Q(serial=None) | Q(serial='') 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') installed = rest_filters.BooleanFilter(label='Installed in other stock item', method='filter_installed')
def filter_installed(self, queryset, name, value): 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): if str2bool(value):
queryset = queryset.exclude(belongs_to=None) queryset = queryset.exclude(belongs_to=None)
else: else:
@ -502,7 +461,7 @@ class StockFilter(rest_filters.FilterSet):
class StockList(APIDownloadMixin, generics.ListCreateAPIView): 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) - GET: Return a list of all StockItem objects (with optional query filters)
- POST: Create a new StockItem - POST: Create a new StockItem
@ -520,15 +479,13 @@ class StockList(APIDownloadMixin, generics.ListCreateAPIView):
return ctx return ctx
def create(self, request, *args, **kwargs): 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. We override the default 'create' implementation.
If a location is *not* specified, but the linked *part* has a default location, If a location is *not* specified, but the linked *part* has a default location,
we can pre-fill the location automatically. we can pre-fill the location automatically.
""" """
user = request.user user = request.user
# Copy the request data, to side-step "mutability" issues # Copy the request data, to side-step "mutability" issues
@ -643,8 +600,8 @@ class StockList(APIDownloadMixin, generics.ListCreateAPIView):
return Response(response_data, status=status.HTTP_201_CREATED, headers=self.get_success_headers(serializer.data)) return Response(response_data, status=status.HTTP_201_CREATED, headers=self.get_success_headers(serializer.data))
def download_queryset(self, queryset, export_format): def download_queryset(self, queryset, export_format):
""" """Download this queryset as a file.
Download this queryset as a file.
Uses the APIDownloadMixin mixin class Uses the APIDownloadMixin mixin class
""" """
dataset = StockItemResource().export(queryset=queryset) dataset = StockItemResource().export(queryset=queryset)
@ -659,13 +616,11 @@ class StockList(APIDownloadMixin, generics.ListCreateAPIView):
return DownloadFile(filedata, filename) return DownloadFile(filedata, filename)
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
""" """Override the 'list' method, as the StockLocation objects
Override the 'list' method, as the StockLocation objects
are very expensive to serialize. are very expensive to serialize.
So, we fetch and serialize the required StockLocation objects only as required. So, we fetch and serialize the required StockLocation objects only as required.
""" """
queryset = self.filter_queryset(self.get_queryset()) queryset = self.filter_queryset(self.get_queryset())
params = request.query_params params = request.query_params
@ -775,9 +730,7 @@ class StockList(APIDownloadMixin, generics.ListCreateAPIView):
return queryset return queryset
def filter_queryset(self, queryset): def filter_queryset(self, queryset):
""" """Custom filtering for the StockItem queryset"""
Custom filtering for the StockItem queryset
"""
params = self.request.query_params params = self.request.query_params
@ -1090,9 +1043,7 @@ class StockList(APIDownloadMixin, generics.ListCreateAPIView):
class StockAttachmentList(generics.ListCreateAPIView, AttachmentMixin): 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() queryset = StockItemAttachment.objects.all()
serializer_class = StockSerializers.StockItemAttachmentSerializer serializer_class = StockSerializers.StockItemAttachmentSerializer
@ -1109,27 +1060,21 @@ class StockAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
class StockAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin): class StockAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin):
""" """Detail endpoint for StockItemAttachment"""
Detail endpoint for StockItemAttachment
"""
queryset = StockItemAttachment.objects.all() queryset = StockItemAttachment.objects.all()
serializer_class = StockSerializers.StockItemAttachmentSerializer serializer_class = StockSerializers.StockItemAttachmentSerializer
class StockItemTestResultDetail(generics.RetrieveUpdateDestroyAPIView): class StockItemTestResultDetail(generics.RetrieveUpdateDestroyAPIView):
""" """Detail endpoint for StockItemTestResult"""
Detail endpoint for StockItemTestResult
"""
queryset = StockItemTestResult.objects.all() queryset = StockItemTestResult.objects.all()
serializer_class = StockSerializers.StockItemTestResultSerializer serializer_class = StockSerializers.StockItemTestResultSerializer
class StockItemTestResultList(generics.ListCreateAPIView): 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() queryset = StockItemTestResult.objects.all()
serializer_class = StockSerializers.StockItemTestResultSerializer serializer_class = StockSerializers.StockItemTestResultSerializer
@ -1205,8 +1150,7 @@ class StockItemTestResultList(generics.ListCreateAPIView):
return self.serializer_class(*args, **kwargs) return self.serializer_class(*args, **kwargs)
def perform_create(self, serializer): 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, Also, check if an attachment was uploaded alongside the test result,
and save it to the database if it were. and save it to the database if it were.
@ -1219,16 +1163,14 @@ class StockItemTestResultList(generics.ListCreateAPIView):
class StockTrackingDetail(generics.RetrieveAPIView): class StockTrackingDetail(generics.RetrieveAPIView):
""" """Detail API endpoint for StockItemTracking model"""
Detail API endpoint for StockItemTracking model
"""
queryset = StockItemTracking.objects.all() queryset = StockItemTracking.objects.all()
serializer_class = StockSerializers.StockTrackingSerializer serializer_class = StockSerializers.StockTrackingSerializer
class StockTrackingList(generics.ListAPIView): 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 StockItemTracking objects are read-only
(they are created by internal model functionality) (they are created by internal model functionality)
@ -1320,7 +1262,7 @@ class StockTrackingList(generics.ListAPIView):
return Response(data) return Response(data)
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
""" Create a new StockItemTracking object """Create a new StockItemTracking object
Here we override the default 'create' implementation, Here we override the default 'create' implementation,
to save the user information associated with the request object. to save the user information associated with the request object.
@ -1374,7 +1316,7 @@ class LocationMetadata(generics.RetrieveUpdateAPIView):
class LocationDetail(generics.RetrieveUpdateDestroyAPIView): 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 - GET: Return a single StockLocation object
- PATCH: Update a StockLocation object - PATCH: Update a StockLocation object

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 from InvenTree.forms import HelperForm
@ -8,8 +6,7 @@ from .models import StockItem, StockItemTracking
class ReturnStockItemForm(HelperForm): 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! TODO: This could be a simple API driven form!
""" """
@ -22,8 +19,7 @@ class ReturnStockItemForm(HelperForm):
class ConvertStockItemForm(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 TODO: Migrate this form to the modern API forms interface
""" """
@ -36,8 +32,7 @@ class ConvertStockItemForm(HelperForm):
class TrackingEntryForm(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? Note: 2021-05-11 - This form is not currently used - should delete?
""" """

View File

@ -1,6 +1,4 @@
""" """Stock database model definitions"""
Stock database model definitions
"""
import os import os
from datetime import datetime, timedelta from datetime import datetime, timedelta
@ -40,17 +38,16 @@ from users.models import Owner
class StockLocation(MetadataMixin, InvenTreeTree): class StockLocation(MetadataMixin, InvenTreeTree):
""" Organization tree for StockItem objects """Organization tree for StockItem objects.
A "StockLocation" can be considered a warehouse, or storage location A "StockLocation" can be considered a warehouse, or storage location
Stock locations can be heirarchical as required Stock locations can be heirarchical as required
""" """
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
""" """Custom model deletion routine, which updates any child locations or items.
Custom model deletion routine, which updates any child locations or items.
This must be handled within a transaction.atomic(), otherwise the tree structure is damaged This must be handled within a transaction.atomic(), otherwise the tree structure is damaged
""" """
with transaction.atomic(): with transaction.atomic():
parent = self.parent parent = self.parent
@ -84,12 +81,10 @@ class StockLocation(MetadataMixin, InvenTreeTree):
related_name='stock_locations') related_name='stock_locations')
def get_location_owner(self): def get_location_owner(self):
""" """Get the closest "owner" for this location.
Get the closest "owner" for this location.
Start at this location, and traverse "up" the location tree until we find an owner Start at this location, and traverse "up" the location tree until we find an owner
""" """
for loc in self.get_ancestors(include_self=True, ascending=True): for loc in self.get_ancestors(include_self=True, ascending=True):
if loc.owner is not None: if loc.owner is not None:
return loc.owner return loc.owner
@ -97,10 +92,7 @@ class StockLocation(MetadataMixin, InvenTreeTree):
return None return None
def check_ownership(self, user): def check_ownership(self, user):
""" """Check if the user "owns" (is one of the owners of) the location."""
Check if the user "owns" (is one of the owners of) the location.
"""
# Superuser accounts automatically "own" everything # Superuser accounts automatically "own" everything
if user.is_superuser: if user.is_superuser:
return True return True
@ -124,8 +116,7 @@ class StockLocation(MetadataMixin, InvenTreeTree):
return reverse('stock-location-detail', kwargs={'pk': self.id}) return reverse('stock-location-detail', kwargs={'pk': self.id})
def format_barcode(self, **kwargs): def format_barcode(self, **kwargs):
""" Return a JSON string for formatting a barcode for this StockLocation object """ """Return a JSON string for formatting a barcode for this StockLocation object"""
return InvenTree.helpers.MakeBarcode( return InvenTree.helpers.MakeBarcode(
'stocklocation', 'stocklocation',
self.pk, self.pk,
@ -138,18 +129,15 @@ class StockLocation(MetadataMixin, InvenTreeTree):
@property @property
def barcode(self): def barcode(self):
""" """Brief payload data (e.g. for labels)"""
Brief payload data (e.g. for labels)
"""
return self.format_barcode(brief=True) return self.format_barcode(brief=True)
def get_stock_items(self, cascade=True): def get_stock_items(self, cascade=True):
""" Return a queryset for all stock items under this category. """Return a queryset for all stock items under this category.
Args: Args:
cascade: If True, also look under sublocations (default = True) cascade: If True, also look under sublocations (default = True)
""" """
if cascade: if cascade:
query = StockItem.objects.filter(location__in=self.getUniqueChildren(include_self=True)) query = StockItem.objects.filter(location__in=self.getUniqueChildren(include_self=True))
else: else:
@ -158,13 +146,11 @@ class StockLocation(MetadataMixin, InvenTreeTree):
return query return query
def stock_item_count(self, cascade=True): def stock_item_count(self, cascade=True):
""" Return the number of StockItem objects which live in or under this category """Return the number of StockItem objects which live in or under this category"""
"""
return self.get_stock_items(cascade).count() return self.get_stock_items(cascade).count()
def has_items(self, cascade=True): def has_items(self, cascade=True):
""" Return True if there are StockItems existing in this category. """Return True if there are StockItems existing in this category.
Args: Args:
cascade: If True, also search an sublocations (default = True) cascade: If True, also search an sublocations (default = True)
@ -173,15 +159,14 @@ class StockLocation(MetadataMixin, InvenTreeTree):
@property @property
def item_count(self): def item_count(self):
""" Simply returns the number of stock items in this location. """Simply returns the number of stock items in this location.
Required for tree view serializer.
""" Required for tree view serializer."""
return self.stock_item_count() return self.stock_item_count()
class StockItemManager(TreeManager): class StockItemManager(TreeManager):
""" """Custom database manager for the StockItem class.
Custom database manager for the StockItem class.
StockItem querysets will automatically prefetch related fields. StockItem querysets will automatically prefetch related fields.
""" """
@ -205,13 +190,11 @@ class StockItemManager(TreeManager):
def generate_batch_code(): def generate_batch_code():
""" """Generate a default 'batch code' for a new StockItem.
Generate a default 'batch code' for a new StockItem.
This uses the value of the 'STOCK_BATCH_CODE_TEMPLATE' setting (if configured), This uses the value of the 'STOCK_BATCH_CODE_TEMPLATE' setting (if configured),
which can be passed through a simple template. which can be passed through a simple template.
""" """
batch_template = common.models.InvenTreeSetting.get_setting('STOCK_BATCH_CODE_TEMPLATE', '') batch_template = common.models.InvenTreeSetting.get_setting('STOCK_BATCH_CODE_TEMPLATE', '')
now = datetime.now() now = datetime.now()
@ -231,8 +214,7 @@ def generate_batch_code():
class StockItem(MetadataMixin, MPTTModel): class StockItem(MetadataMixin, MPTTModel):
""" """A StockItem object represents a quantity of physical instances of a part.
A StockItem object represents a quantity of physical instances of a part.
Attributes: Attributes:
parent: Link to another StockItem from which this StockItem was created parent: Link to another StockItem from which this StockItem was created
@ -290,11 +272,10 @@ class StockItem(MetadataMixin, MPTTModel):
EXPIRED_FILTER = IN_STOCK_FILTER & ~Q(expiry_date=None) & Q(expiry_date__lt=datetime.now().date()) EXPIRED_FILTER = IN_STOCK_FILTER & ~Q(expiry_date=None) & Q(expiry_date__lt=datetime.now().date())
def update_serial_number(self): def update_serial_number(self):
""" """Update the 'serial_int' field, to be an integer representation of the serial number.
Update the 'serial_int' field, to be an integer representation of the serial number.
This is used for efficient numerical sorting This is used for efficient numerical sorting
""" """
serial = getattr(self, 'serial', '') serial = getattr(self, 'serial', '')
# Default value if we cannot convert to an integer # Default value if we cannot convert to an integer
@ -309,8 +290,7 @@ class StockItem(MetadataMixin, MPTTModel):
self.serial_int = serial_int self.serial_int = serial_int
def get_next_serialized_item(self, include_variants=True, reverse=False): def get_next_serialized_item(self, include_variants=True, reverse=False):
""" """Get the "next" serial number for the part this stock item references.
Get the "next" serial number for the part this stock item references.
e.g. if this stock item has a serial number 100, we may return the stock item with serial number 101 e.g. if this stock item has a serial number 100, we may return the stock item with serial number 101
@ -322,9 +302,7 @@ class StockItem(MetadataMixin, MPTTModel):
Returns: Returns:
A StockItem object matching the requirements, or None A StockItem object matching the requirements, or None
""" """
if not self.serialized: if not self.serialized:
return None return None
@ -358,13 +336,11 @@ class StockItem(MetadataMixin, MPTTModel):
return None return None
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
""" """Save this StockItem to the database. Performs a number of checks:
Save this StockItem to the database. Performs a number of checks:
- Unique serial number requirement - Unique serial number requirement
- Adds a transaction note when the item is first created. - Adds a transaction note when the item is first created.
""" """
self.validate_unique() self.validate_unique()
self.clean() self.clean()
@ -433,16 +409,15 @@ class StockItem(MetadataMixin, MPTTModel):
@property @property
def serialized(self): def serialized(self):
""" Return True if this StockItem is serialized """ """Return True if this StockItem is serialized"""
return self.serial is not None and len(str(self.serial).strip()) > 0 and self.quantity == 1 return self.serial is not None and len(str(self.serial).strip()) > 0 and self.quantity == 1
def validate_unique(self, exclude=None): def validate_unique(self, exclude=None):
""" """Test that this StockItem is "unique".
Test that this StockItem is "unique".
If the StockItem is serialized, the same serial number. If the StockItem is serialized, the same serial number.
cannot exist for the same part (or part tree). cannot exist for the same part (or part tree).
""" """
super(StockItem, self).validate_unique(exclude) super(StockItem, self).validate_unique(exclude)
# If the serial number is set, make sure it is not a duplicate # If the serial number is set, make sure it is not a duplicate
@ -459,7 +434,7 @@ class StockItem(MetadataMixin, MPTTModel):
raise ValidationError({"serial": _("StockItem with this serial number already exists")}) raise ValidationError({"serial": _("StockItem with this serial number already exists")})
def clean(self): def clean(self):
""" Validate the StockItem object (separate to field validation) """Validate the StockItem object (separate to field validation)
The following validation checks are performed: The following validation checks are performed:
@ -467,7 +442,6 @@ class StockItem(MetadataMixin, MPTTModel):
- The 'part' does not belong to itself - The 'part' does not belong to itself
- Quantity must be 1 if the StockItem has a serial number - Quantity must be 1 if the StockItem has a serial number
""" """
super().clean() super().clean()
# Strip serial number field # Strip serial number field
@ -563,7 +537,7 @@ class StockItem(MetadataMixin, MPTTModel):
return self.part.full_name return self.part.full_name
def format_barcode(self, **kwargs): def format_barcode(self, **kwargs):
""" Return a JSON string for formatting a barcode for this StockItem. """Return a JSON string for formatting a barcode for this StockItem.
Can be used to perform lookup of a stockitem using barcode Can be used to perform lookup of a stockitem using barcode
Contains the following data: Contains the following data:
@ -572,7 +546,6 @@ class StockItem(MetadataMixin, MPTTModel):
Voltagile data (e.g. stock quantity) should be looked up using the InvenTree API (as it may change) Voltagile data (e.g. stock quantity) should be looked up using the InvenTree API (as it may change)
""" """
return InvenTree.helpers.MakeBarcode( return InvenTree.helpers.MakeBarcode(
"stockitem", "stockitem",
self.id, self.id,
@ -586,9 +559,7 @@ class StockItem(MetadataMixin, MPTTModel):
@property @property
def barcode(self): def barcode(self):
""" """Brief payload data (e.g. for labels)"""
Brief payload data (e.g. for labels)
"""
return self.format_barcode(brief=True) return self.format_barcode(brief=True)
uid = models.CharField(blank=True, max_length=128, help_text=("Unique identifier field")) uid = models.CharField(blank=True, max_length=128, help_text=("Unique identifier field"))
@ -753,11 +724,7 @@ class StockItem(MetadataMixin, MPTTModel):
@transaction.atomic @transaction.atomic
def convert_to_variant(self, variant, user, notes=None): def convert_to_variant(self, variant, user, notes=None):
""" """Convert this StockItem instance to a "variant", i.e. change the "part" reference field"""
Convert this StockItem instance to a "variant",
i.e. change the "part" reference field
"""
if not variant: if not variant:
# Ignore null values # Ignore null values
return return
@ -779,14 +746,12 @@ class StockItem(MetadataMixin, MPTTModel):
) )
def get_item_owner(self): def get_item_owner(self):
""" """Return the closest "owner" for this StockItem.
Return the closest "owner" for this StockItem.
- If the item has an owner set, return that - If the item has an owner set, return that
- If the item is "in stock", check the StockLocation - If the item is "in stock", check the StockLocation
- Otherwise, return None - Otherwise, return None
""" """
if self.owner is not None: if self.owner is not None:
return self.owner return self.owner
@ -799,10 +764,7 @@ class StockItem(MetadataMixin, MPTTModel):
return None return None
def check_ownership(self, user): def check_ownership(self, user):
""" """Check if the user "owns" (or is one of the owners of) the item"""
Check if the user "owns" (or is one of the owners of) the item
"""
# Superuser accounts automatically "own" everything # Superuser accounts automatically "own" everything
if user.is_superuser: if user.is_superuser:
return True return True
@ -821,8 +783,7 @@ class StockItem(MetadataMixin, MPTTModel):
return user in owner.get_related_owners(include_group=True) return user in owner.get_related_owners(include_group=True)
def is_stale(self): def is_stale(self):
""" """Returns True if this Stock item is "stale".
Returns True if this Stock item is "stale".
To be "stale", the following conditions must be met: To be "stale", the following conditions must be met:
@ -830,7 +791,6 @@ class StockItem(MetadataMixin, MPTTModel):
- Expiry date will "expire" within the configured stale date - Expiry date will "expire" within the configured stale date
- The StockItem is otherwise "in stock" - The StockItem is otherwise "in stock"
""" """
if self.expiry_date is None: if self.expiry_date is None:
return False return False
@ -849,8 +809,7 @@ class StockItem(MetadataMixin, MPTTModel):
return self.expiry_date < expiry_date return self.expiry_date < expiry_date
def is_expired(self): def is_expired(self):
""" """Returns True if this StockItem is "expired".
Returns True if this StockItem is "expired".
To be "expired", the following conditions must be met: To be "expired", the following conditions must be met:
@ -858,7 +817,6 @@ class StockItem(MetadataMixin, MPTTModel):
- Expiry date is "in the past" - Expiry date is "in the past"
- The StockItem is otherwise "in stock" - The StockItem is otherwise "in stock"
""" """
if self.expiry_date is None: if self.expiry_date is None:
return False return False
@ -870,13 +828,11 @@ class StockItem(MetadataMixin, MPTTModel):
return self.expiry_date < today return self.expiry_date < today
def clearAllocations(self): def clearAllocations(self):
""" """Clear all order allocations for this StockItem:
Clear all order allocations for this StockItem:
- SalesOrder allocations - SalesOrder allocations
- Build allocations - Build allocations
""" """
# Delete outstanding SalesOrder allocations # Delete outstanding SalesOrder allocations
self.sales_order_allocations.all().delete() self.sales_order_allocations.all().delete()
@ -884,8 +840,7 @@ class StockItem(MetadataMixin, MPTTModel):
self.allocations.all().delete() self.allocations.all().delete()
def allocateToCustomer(self, customer, quantity=None, order=None, user=None, notes=None): def allocateToCustomer(self, customer, quantity=None, order=None, user=None, notes=None):
""" """Allocate a StockItem to a customer.
Allocate a StockItem to a customer.
This action can be called by the following processes: This action can be called by the following processes:
- Completion of a SalesOrder - Completion of a SalesOrder
@ -898,7 +853,6 @@ class StockItem(MetadataMixin, MPTTModel):
user: User that performed the action user: User that performed the action
notes: Notes field notes: Notes field
""" """
if quantity is None: if quantity is None:
quantity = self.quantity quantity = self.quantity
@ -936,10 +890,7 @@ class StockItem(MetadataMixin, MPTTModel):
return item return item
def returnFromCustomer(self, location, user=None, **kwargs): def returnFromCustomer(self, location, user=None, **kwargs):
""" """Return stock item from customer, back into the specified location."""
Return stock item from customer, back into the specified location.
"""
notes = kwargs.get('notes', '') notes = kwargs.get('notes', '')
tracking_info = {} tracking_info = {}
@ -972,10 +923,7 @@ class StockItem(MetadataMixin, MPTTModel):
infinite = models.BooleanField(default=False) infinite = models.BooleanField(default=False)
def is_allocated(self): def is_allocated(self):
""" """Return True if this StockItem is allocated to a SalesOrder or a Build"""
Return True if this StockItem is allocated to a SalesOrder or a Build
"""
# TODO - For now this only checks if the StockItem is allocated to a SalesOrder # TODO - For now this only checks if the StockItem is allocated to a SalesOrder
# TODO - In future, once the "build" is working better, check this too # TODO - In future, once the "build" is working better, check this too
@ -988,10 +936,7 @@ class StockItem(MetadataMixin, MPTTModel):
return False return False
def build_allocation_count(self): def build_allocation_count(self):
""" """Return the total quantity allocated to builds"""
Return the total quantity allocated to builds
"""
query = self.allocations.aggregate(q=Coalesce(Sum('quantity'), Decimal(0))) query = self.allocations.aggregate(q=Coalesce(Sum('quantity'), Decimal(0)))
total = query['q'] total = query['q']
@ -1002,10 +947,7 @@ class StockItem(MetadataMixin, MPTTModel):
return total return total
def sales_order_allocation_count(self): def sales_order_allocation_count(self):
""" """Return the total quantity allocated to SalesOrders"""
Return the total quantity allocated to SalesOrders
"""
query = self.sales_order_allocations.aggregate(q=Coalesce(Sum('quantity'), Decimal(0))) query = self.sales_order_allocations.aggregate(q=Coalesce(Sum('quantity'), Decimal(0)))
total = query['q'] total = query['q']
@ -1016,31 +958,24 @@ class StockItem(MetadataMixin, MPTTModel):
return total return total
def allocation_count(self): def allocation_count(self):
""" """Return the total quantity allocated to builds or orders"""
Return the total quantity allocated to builds or orders
"""
bo = self.build_allocation_count() bo = self.build_allocation_count()
so = self.sales_order_allocation_count() so = self.sales_order_allocation_count()
return bo + so return bo + so
def unallocated_quantity(self): def unallocated_quantity(self):
""" """Return the quantity of this StockItem which is *not* allocated"""
Return the quantity of this StockItem which is *not* allocated
"""
return max(self.quantity - self.allocation_count(), 0) return max(self.quantity - self.allocation_count(), 0)
def can_delete(self): def can_delete(self):
""" Can this stock item be deleted? It can NOT be deleted under the following circumstances: """Can this stock item be deleted? It can NOT be deleted under the following circumstances:
- Has installed stock items - Has installed stock items
- Is installed inside another StockItem - Is installed inside another StockItem
- It has been assigned to a SalesOrder - It has been assigned to a SalesOrder
- It has been assigned to a BuildOrder - It has been assigned to a BuildOrder
""" """
if self.installed_item_count() > 0: if self.installed_item_count() > 0:
return False return False
@ -1050,15 +985,13 @@ class StockItem(MetadataMixin, MPTTModel):
return True return True
def get_installed_items(self, cascade=False): def get_installed_items(self, cascade=False):
""" """Return all stock items which are *installed* in this one!
Return all stock items which are *installed* in this one!
Args: Args:
cascade - Include items which are installed in items which are installed in items cascade - Include items which are installed in items which are installed in items
Note: This function is recursive, and may result in a number of database hits! Note: This function is recursive, and may result in a number of database hits!
""" """
installed = set() installed = set()
items = StockItem.objects.filter(belongs_to=self) items = StockItem.objects.filter(belongs_to=self)
@ -1085,16 +1018,12 @@ class StockItem(MetadataMixin, MPTTModel):
return installed return installed
def installed_item_count(self): def installed_item_count(self):
""" """Return the number of stock items installed inside this one."""
Return the number of stock items installed inside this one.
"""
return self.installed_parts.count() return self.installed_parts.count()
@transaction.atomic @transaction.atomic
def installStockItem(self, other_item, quantity, user, notes): def installStockItem(self, other_item, quantity, user, notes):
""" """Install another stock item into this stock item.
Install another stock item into this stock item.
Args Args
other_item: The stock item to install into this stock item other_item: The stock item to install into this stock item
@ -1102,7 +1031,6 @@ class StockItem(MetadataMixin, MPTTModel):
user: The user performing the operation user: The user performing the operation
notes: Any notes associated with the operation notes: Any notes associated with the operation
""" """
# Cannot be already installed in another stock item! # Cannot be already installed in another stock item!
if self.belongs_to is not None: if self.belongs_to is not None:
return False return False
@ -1139,15 +1067,13 @@ class StockItem(MetadataMixin, MPTTModel):
@transaction.atomic @transaction.atomic
def uninstall_into_location(self, location, user, notes): def uninstall_into_location(self, location, user, notes):
""" """Uninstall this stock item from another item, into a location.
Uninstall this stock item from another item, into a location.
Args: Args:
location: The stock location where the item will be moved location: The stock location where the item will be moved
user: The user performing the operation user: The user performing the operation
notes: Any notes associated with the operation notes: Any notes associated with the operation
""" """
# If the stock item is not installed in anything, ignore # If the stock item is not installed in anything, ignore
if self.belongs_to is None: if self.belongs_to is None:
return False return False
@ -1184,24 +1110,22 @@ class StockItem(MetadataMixin, MPTTModel):
@property @property
def children(self): def children(self):
""" Return a list of the child items which have been split from this stock item """ """Return a list of the child items which have been split from this stock item"""
return self.get_descendants(include_self=False) return self.get_descendants(include_self=False)
@property @property
def child_count(self): def child_count(self):
""" Return the number of 'child' items associated with this StockItem. """Return the number of 'child' items associated with this StockItem.
A child item is one which has been split from this one.
""" A child item is one which has been split from this one."""
return self.children.count() return self.children.count()
@property @property
def in_stock(self): def in_stock(self):
""" """Returns True if this item is in stock.
Returns True if this item is in stock.
See also: IN_STOCK_FILTER See also: IN_STOCK_FILTER
""" """
query = StockItem.objects.filter(pk=self.pk) query = StockItem.objects.filter(pk=self.pk)
query = query.filter(StockItem.IN_STOCK_FILTER) query = query.filter(StockItem.IN_STOCK_FILTER)
@ -1210,14 +1134,12 @@ class StockItem(MetadataMixin, MPTTModel):
@property @property
def can_adjust_location(self): def can_adjust_location(self):
""" """Returns True if the stock location can be "adjusted" for this part
Returns True if the stock location can be "adjusted" for this part
Cannot be adjusted if: Cannot be adjusted if:
- Has been delivered to a customer - Has been delivered to a customer
- Has been installed inside another StockItem - Has been installed inside another StockItem
""" """
if self.customer is not None: if self.customer is not None:
return False return False
@ -1238,8 +1160,7 @@ class StockItem(MetadataMixin, MPTTModel):
return self.tracking_info_count > 0 return self.tracking_info_count > 0
def add_tracking_entry(self, entry_type, user, deltas=None, notes='', **kwargs): def add_tracking_entry(self, entry_type, user, deltas=None, notes='', **kwargs):
""" """Add a history tracking entry for this StockItem
Add a history tracking entry for this StockItem
Args: Args:
entry_type - Integer code describing the "type" of historical action (see StockHistoryCode) entry_type - Integer code describing the "type" of historical action (see StockHistoryCode)
@ -1276,7 +1197,7 @@ class StockItem(MetadataMixin, MPTTModel):
@transaction.atomic @transaction.atomic
def serializeStock(self, quantity, serials, user, notes='', location=None): def serializeStock(self, quantity, serials, user, notes='', location=None):
""" Split this stock item into unique serial numbers. """Split this stock item into unique serial numbers.
- Quantity can be less than or equal to the quantity of the stock item - Quantity can be less than or equal to the quantity of the stock item
- Number of serial numbers must match the quantity - Number of serial numbers must match the quantity
@ -1289,7 +1210,6 @@ class StockItem(MetadataMixin, MPTTModel):
notes: Optional notes for tracking notes: Optional notes for tracking
location: If specified, serialized items will be placed in the given location location: If specified, serialized items will be placed in the given location
""" """
# Cannot serialize stock that is already serialized! # Cannot serialize stock that is already serialized!
if self.serialized: if self.serialized:
return return
@ -1360,8 +1280,7 @@ class StockItem(MetadataMixin, MPTTModel):
@transaction.atomic @transaction.atomic
def copyHistoryFrom(self, other): def copyHistoryFrom(self, other):
""" Copy stock history from another StockItem """ """Copy stock history from another StockItem"""
for item in other.tracking_info.all(): for item in other.tracking_info.all():
item.item = self item.item = self
@ -1370,8 +1289,7 @@ class StockItem(MetadataMixin, MPTTModel):
@transaction.atomic @transaction.atomic
def copyTestResultsFrom(self, other, filters={}): def copyTestResultsFrom(self, other, filters={}):
""" Copy all test results from another StockItem """ """Copy all test results from another StockItem"""
for result in other.test_results.all().filter(**filters): for result in other.test_results.all().filter(**filters):
# Create a copy of the test result by nulling-out the pk # Create a copy of the test result by nulling-out the pk
@ -1380,10 +1298,7 @@ class StockItem(MetadataMixin, MPTTModel):
result.save() result.save()
def can_merge(self, other=None, raise_error=False, **kwargs): def can_merge(self, other=None, raise_error=False, **kwargs):
""" """Check if this stock item can be merged into another stock item"""
Check if this stock item can be merged into another stock item
"""
allow_mismatched_suppliers = kwargs.get('allow_mismatched_suppliers', False) allow_mismatched_suppliers = kwargs.get('allow_mismatched_suppliers', False)
allow_mismatched_status = kwargs.get('allow_mismatched_status', False) allow_mismatched_status = kwargs.get('allow_mismatched_status', False)
@ -1437,8 +1352,7 @@ class StockItem(MetadataMixin, MPTTModel):
@transaction.atomic @transaction.atomic
def merge_stock_items(self, other_items, raise_error=False, **kwargs): def merge_stock_items(self, other_items, raise_error=False, **kwargs):
""" """Merge another stock item into this one; the two become one!
Merge another stock item into this one; the two become one!
*This* stock item subsumes the other, which is essentially deleted: *This* stock item subsumes the other, which is essentially deleted:
@ -1446,7 +1360,6 @@ class StockItem(MetadataMixin, MPTTModel):
- Tracking history for the *other* item is deleted - Tracking history for the *other* item is deleted
- Any allocations (build order, sales order) are moved to this StockItem - Any allocations (build order, sales order) are moved to this StockItem
""" """
if len(other_items) == 0: if len(other_items) == 0:
return return
@ -1499,7 +1412,8 @@ class StockItem(MetadataMixin, MPTTModel):
@transaction.atomic @transaction.atomic
def splitStock(self, quantity, location, user, **kwargs): def splitStock(self, quantity, location, user, **kwargs):
""" Split this stock item into two items, in the same location. """Split this stock item into two items, in the same location.
Stock tracking notes for this StockItem will be duplicated, Stock tracking notes for this StockItem will be duplicated,
and added to the new StockItem. and added to the new StockItem.
@ -1511,7 +1425,6 @@ class StockItem(MetadataMixin, MPTTModel):
The provided quantity will be subtracted from this item and given to the new one. The provided quantity will be subtracted from this item and given to the new one.
The new item will have a different StockItem ID, while this will remain the same. The new item will have a different StockItem ID, while this will remain the same.
""" """
notes = kwargs.get('notes', '') notes = kwargs.get('notes', '')
code = kwargs.get('code', StockHistoryCode.SPLIT_FROM_PARENT) code = kwargs.get('code', StockHistoryCode.SPLIT_FROM_PARENT)
@ -1576,7 +1489,7 @@ class StockItem(MetadataMixin, MPTTModel):
@transaction.atomic @transaction.atomic
def move(self, location, notes, user, **kwargs): def move(self, location, notes, user, **kwargs):
""" Move part to a new location. """Move part to a new location.
If less than the available quantity is to be moved, If less than the available quantity is to be moved,
a new StockItem is created, with the defined quantity, a new StockItem is created, with the defined quantity,
@ -1590,7 +1503,6 @@ class StockItem(MetadataMixin, MPTTModel):
kwargs: kwargs:
quantity: If provided, override the quantity (default = total stock quantity) quantity: If provided, override the quantity (default = total stock quantity)
""" """
try: try:
quantity = Decimal(kwargs.get('quantity', self.quantity)) quantity = Decimal(kwargs.get('quantity', self.quantity))
except InvalidOperation: except InvalidOperation:
@ -1636,7 +1548,7 @@ class StockItem(MetadataMixin, MPTTModel):
@transaction.atomic @transaction.atomic
def updateQuantity(self, quantity): def updateQuantity(self, quantity):
""" Update stock quantity for this item. """Update stock quantity for this item.
If the quantity has reached zero, this StockItem will be deleted. If the quantity has reached zero, this StockItem will be deleted.
@ -1644,7 +1556,6 @@ class StockItem(MetadataMixin, MPTTModel):
- True if the quantity was saved - True if the quantity was saved
- False if the StockItem was deleted - False if the StockItem was deleted
""" """
# Do not adjust quantity of a serialized part # Do not adjust quantity of a serialized part
if self.serialized: if self.serialized:
return return
@ -1669,11 +1580,10 @@ class StockItem(MetadataMixin, MPTTModel):
@transaction.atomic @transaction.atomic
def stocktake(self, count, user, notes=''): def stocktake(self, count, user, notes=''):
""" Perform item stocktake. """Perform item stocktake.
When the quantity of an item is counted, When the quantity of an item is counted,
record the date of stocktake record the date of stocktake
""" """
try: try:
count = Decimal(count) count = Decimal(count)
except InvalidOperation: except InvalidOperation:
@ -1700,11 +1610,10 @@ class StockItem(MetadataMixin, MPTTModel):
@transaction.atomic @transaction.atomic
def add_stock(self, quantity, user, notes=''): def add_stock(self, quantity, user, notes=''):
""" Add items to stock """Add items to stock
This function can be called by initiating a ProjectRun, This function can be called by initiating a ProjectRun,
or by manually adding the items to the stock location or by manually adding the items to the stock location
""" """
# Cannot add items to a serialized part # Cannot add items to a serialized part
if self.serialized: if self.serialized:
return False return False
@ -1734,10 +1643,7 @@ class StockItem(MetadataMixin, MPTTModel):
@transaction.atomic @transaction.atomic
def take_stock(self, quantity, user, notes='', code=StockHistoryCode.STOCK_REMOVE): def take_stock(self, quantity, user, notes='', code=StockHistoryCode.STOCK_REMOVE):
""" """Remove items from stock"""
Remove items from stock
"""
# Cannot remove items from a serialized part # Cannot remove items from a serialized part
if self.serialized: if self.serialized:
return False return False
@ -1787,13 +1693,7 @@ class StockItem(MetadataMixin, MPTTModel):
@transaction.atomic @transaction.atomic
def clear_test_results(self, **kwargs): def clear_test_results(self, **kwargs):
""" """Remove all test results"""
Remove all test results
kwargs:
TODO
"""
# All test results # All test results
results = self.test_results.all() results = self.test_results.all()
@ -1802,15 +1702,13 @@ class StockItem(MetadataMixin, MPTTModel):
results.delete() results.delete()
def getTestResults(self, test=None, result=None, user=None): def getTestResults(self, test=None, result=None, user=None):
""" """Return all test results associated with this StockItem.
Return all test results associated with this StockItem.
Optionally can filter results by: Optionally can filter results by:
- Test name - Test name
- Test result - Test result
- User - User
""" """
results = self.test_results results = self.test_results
if test: if test:
@ -1828,15 +1726,13 @@ class StockItem(MetadataMixin, MPTTModel):
return results return results
def testResultMap(self, **kwargs): def testResultMap(self, **kwargs):
""" """Return a map of test-results using the test name as the key.
Return a map of test-results using the test name as the key.
Where multiple test results exist for a given name, Where multiple test results exist for a given name,
the *most recent* test is used. the *most recent* test is used.
This map is useful for rendering to a template (e.g. a test report), This map is useful for rendering to a template (e.g. a test report),
as all named tests are accessible. as all named tests are accessible.
""" """
# Do we wish to include test results from installed items? # Do we wish to include test results from installed items?
include_installed = kwargs.pop('include_installed', False) include_installed = kwargs.pop('include_installed', False)
@ -1867,15 +1763,11 @@ class StockItem(MetadataMixin, MPTTModel):
return result_map return result_map
def testResultList(self, **kwargs): def testResultList(self, **kwargs):
""" """Return a list of test-result objects for this StockItem"""
Return a list of test-result objects for this StockItem
"""
return self.testResultMap(**kwargs).values() return self.testResultMap(**kwargs).values()
def requiredTestStatus(self): def requiredTestStatus(self):
""" """Return the status of the tests required for this StockItem.
Return the status of the tests required for this StockItem.
return: return:
A dict containing the following items: A dict containing the following items:
@ -1883,7 +1775,6 @@ class StockItem(MetadataMixin, MPTTModel):
- passed: Number of tests that have passed - passed: Number of tests that have passed
- failed: Number of tests that have failed - failed: Number of tests that have failed
""" """
# All the tests required by the part object # All the tests required by the part object
required = self.part.getRequiredTests() required = self.part.getRequiredTests()
@ -1912,31 +1803,21 @@ class StockItem(MetadataMixin, MPTTModel):
@property @property
def required_test_count(self): def required_test_count(self):
""" """Return the number of 'required tests' for this StockItem"""
Return the number of 'required tests' for this StockItem
"""
return self.part.getRequiredTests().count() return self.part.getRequiredTests().count()
def hasRequiredTests(self): def hasRequiredTests(self):
""" """Return True if there are any 'required tests' associated with this StockItem"""
Return True if there are any 'required tests' associated with this StockItem
"""
return self.part.getRequiredTests().count() > 0 return self.part.getRequiredTests().count() > 0
def passedAllRequiredTests(self): def passedAllRequiredTests(self):
""" """Returns True if this StockItem has passed all required tests"""
Returns True if this StockItem has passed all required tests
"""
status = self.requiredTestStatus() status = self.requiredTestStatus()
return status['passed'] >= status['total'] return status['passed'] >= status['total']
def available_test_reports(self): def available_test_reports(self):
""" """Return a list of TestReport objects which match this StockItem."""
Return a list of TestReport objects which match this StockItem.
"""
reports = [] reports = []
item_query = StockItem.objects.filter(pk=self.pk) item_query = StockItem.objects.filter(pk=self.pk)
@ -1955,17 +1836,11 @@ class StockItem(MetadataMixin, MPTTModel):
@property @property
def has_test_reports(self): def has_test_reports(self):
""" """Return True if there are test reports available for this stock item"""
Return True if there are test reports available for this stock item
"""
return len(self.available_test_reports()) > 0 return len(self.available_test_reports()) > 0
def available_labels(self): def available_labels(self):
""" """Return a list of Label objects which match this StockItem"""
Return a list of Label objects which match this StockItem
"""
labels = [] labels = []
item_query = StockItem.objects.filter(pk=self.pk) item_query = StockItem.objects.filter(pk=self.pk)
@ -1984,22 +1859,17 @@ class StockItem(MetadataMixin, MPTTModel):
@property @property
def has_labels(self): def has_labels(self):
""" """Return True if there are any label templates available for this stock item"""
Return True if there are any label templates available for this stock item
"""
return len(self.available_labels()) > 0 return len(self.available_labels()) > 0
@receiver(pre_delete, sender=StockItem, dispatch_uid='stock_item_pre_delete_log') @receiver(pre_delete, sender=StockItem, dispatch_uid='stock_item_pre_delete_log')
def before_delete_stock_item(sender, instance, using, **kwargs): def before_delete_stock_item(sender, instance, using, **kwargs):
""" """Receives pre_delete signal from StockItem object.
Receives pre_delete signal from StockItem object.
Before a StockItem is deleted, ensure that each child object is updated, Before a StockItem is deleted, ensure that each child object is updated,
to point to the new parent item. to point to the new parent item.
""" """
# Update each StockItem parent field # Update each StockItem parent field
for child in instance.children.all(): for child in instance.children.all():
child.parent = instance.parent child.parent = instance.parent
@ -2008,9 +1878,7 @@ def before_delete_stock_item(sender, instance, using, **kwargs):
@receiver(post_delete, sender=StockItem, dispatch_uid='stock_item_post_delete_log') @receiver(post_delete, sender=StockItem, dispatch_uid='stock_item_post_delete_log')
def after_delete_stock_item(sender, instance: StockItem, **kwargs): def after_delete_stock_item(sender, instance: StockItem, **kwargs):
""" """Function to be executed after a StockItem object is deleted"""
Function to be executed after a StockItem object is deleted
"""
from part import tasks as part_tasks from part import tasks as part_tasks
if not InvenTree.ready.isImportingData(): if not InvenTree.ready.isImportingData():
@ -2020,9 +1888,7 @@ def after_delete_stock_item(sender, instance: StockItem, **kwargs):
@receiver(post_save, sender=StockItem, dispatch_uid='stock_item_post_save_log') @receiver(post_save, sender=StockItem, dispatch_uid='stock_item_post_save_log')
def after_save_stock_item(sender, instance: StockItem, created, **kwargs): def after_save_stock_item(sender, instance: StockItem, created, **kwargs):
""" """Hook function to be executed after StockItem object is saved/updated"""
Hook function to be executed after StockItem object is saved/updated
"""
from part import tasks as part_tasks from part import tasks as part_tasks
if not InvenTree.ready.isImportingData(): if not InvenTree.ready.isImportingData():
@ -2050,8 +1916,7 @@ class StockItemAttachment(InvenTreeAttachment):
class StockItemTracking(models.Model): class StockItemTracking(models.Model):
""" """Stock tracking entry - used for tracking history of a particular StockItem
Stock tracking entry - used for tracking history of a particular StockItem
Note: 2021-05-11 Note: 2021-05-11
The legacy StockTrackingItem model contained very litle information about the "history" of the item. The legacy StockTrackingItem model contained very litle information about the "history" of the item.
@ -2114,8 +1979,7 @@ def rename_stock_item_test_result_attachment(instance, filename):
class StockItemTestResult(models.Model): class StockItemTestResult(models.Model):
""" """A StockItemTestResult records results of custom tests against individual StockItem objects.
A StockItemTestResult records results of custom tests against individual StockItem objects.
This is useful for tracking unit acceptance tests, and particularly useful when integrated This is useful for tracking unit acceptance tests, and particularly useful when integrated
with automated testing setups. with automated testing setups.

View File

@ -1,6 +1,4 @@
""" """JSON serializers for Stock app"""
JSON serializers for Stock app
"""
from datetime import datetime, timedelta from datetime import datetime, timedelta
from decimal import Decimal from decimal import Decimal
@ -29,9 +27,7 @@ from .models import (StockItem, StockItemAttachment, StockItemTestResult,
class LocationBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer): class LocationBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer):
""" """Provides a brief serializer for a StockLocation object"""
Provides a brief serializer for a StockLocation object
"""
class Meta: class Meta:
model = StockLocation model = StockLocation
@ -43,7 +39,7 @@ class LocationBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer):
class StockItemSerializerBrief(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) location_name = serializers.CharField(source='location', read_only=True)
part_name = serializers.CharField(source='part.full_name', read_only=True) part_name = serializers.CharField(source='part.full_name', read_only=True)
@ -71,7 +67,7 @@ class StockItemSerializerBrief(InvenTree.serializers.InvenTreeModelSerializer):
class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer): class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
""" Serializer for a StockItem: """Serializer for a StockItem:
- Includes serialization for the linked part - Includes serialization for the linked part
- Includes serialization for the item location - Includes serialization for the item location
@ -88,11 +84,7 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
@staticmethod @staticmethod
def annotate_queryset(queryset): 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 # Annotate the queryset with the total allocated to sales orders
queryset = queryset.annotate( queryset = queryset.annotate(
allocated=Coalesce( allocated=Coalesce(
@ -257,8 +249,7 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
class SerializeStockItemSerializer(serializers.Serializer): class SerializeStockItemSerializer(serializers.Serializer):
""" """A DRF serializer for "serializing" a StockItem.
A DRF serializer for "serializing" a StockItem.
(Sorry for the confusing naming...) (Sorry for the confusing naming...)
@ -284,9 +275,7 @@ class SerializeStockItemSerializer(serializers.Serializer):
) )
def validate_quantity(self, quantity): def validate_quantity(self, quantity):
""" """Validate that the quantity value is correct"""
Validate that the quantity value is correct
"""
item = self.context['item'] item = self.context['item']
@ -323,9 +312,7 @@ class SerializeStockItemSerializer(serializers.Serializer):
) )
def validate(self, data): def validate(self, data):
""" """Check that the supplied serial numbers are valid"""
Check that the supplied serial numbers are valid
"""
data = super().validate(data) data = super().validate(data)
@ -381,9 +368,7 @@ class SerializeStockItemSerializer(serializers.Serializer):
class InstallStockItemSerializer(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( stock_item = serializers.PrimaryKeyRelatedField(
queryset=StockItem.objects.all(), queryset=StockItem.objects.all(),
@ -401,9 +386,7 @@ class InstallStockItemSerializer(serializers.Serializer):
) )
def validate_stock_item(self, stock_item): def validate_stock_item(self, stock_item):
""" """Validate the selected stock item"""
Validate the selected stock item
"""
if not stock_item.in_stock: if not stock_item.in_stock:
# StockItem must be in stock to be "installed" # StockItem must be in stock to be "installed"
@ -419,7 +402,7 @@ class InstallStockItemSerializer(serializers.Serializer):
return stock_item return stock_item
def save(self): def save(self):
""" Install the selected stock item into this one """ """Install the selected stock item into this one"""
data = self.validated_data data = self.validated_data
@ -438,9 +421,7 @@ class InstallStockItemSerializer(serializers.Serializer):
class UninstallStockItemSerializer(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: class Meta:
fields = [ fields = [
@ -480,9 +461,7 @@ class UninstallStockItemSerializer(serializers.Serializer):
class LocationTreeSerializer(InvenTree.serializers.InvenTreeModelSerializer): class LocationTreeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
""" """Serializer for a simple tree view"""
Serializer for a simple tree view
"""
class Meta: class Meta:
model = StockLocation model = StockLocation
@ -494,8 +473,7 @@ class LocationTreeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
class LocationSerializer(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) url = serializers.CharField(source='get_absolute_url', read_only=True)
@ -519,7 +497,7 @@ class LocationSerializer(InvenTree.serializers.InvenTreeModelSerializer):
class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSerializer): class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSerializer):
""" Serializer for StockItemAttachment model """ """Serializer for StockItemAttachment model"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
user_detail = kwargs.pop('user_detail', False) user_detail = kwargs.pop('user_detail', False)
@ -556,7 +534,7 @@ class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSer
class StockItemTestResultSerializer(InvenTree.serializers.InvenTreeModelSerializer): 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) user_detail = InvenTree.serializers.UserSerializerBrief(source='user', read_only=True)
@ -597,7 +575,7 @@ class StockItemTestResultSerializer(InvenTree.serializers.InvenTreeModelSerializ
class StockTrackingSerializer(InvenTree.serializers.InvenTreeModelSerializer): class StockTrackingSerializer(InvenTree.serializers.InvenTreeModelSerializer):
""" Serializer for StockItemTracking model """ """Serializer for StockItemTracking model"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -644,8 +622,7 @@ class StockTrackingSerializer(InvenTree.serializers.InvenTreeModelSerializer):
class StockAssignmentItemSerializer(serializers.Serializer): 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 Here, the particular StockItem is being assigned (manually) to a customer
@ -688,8 +665,7 @@ class StockAssignmentItemSerializer(serializers.Serializer):
class StockAssignmentSerializer(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 This is a manual assignment process, separate for (for example) a Sales Order
""" """
@ -765,8 +741,7 @@ class StockAssignmentSerializer(serializers.Serializer):
class StockMergeItemSerializer(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. Here, the individual StockItem is being checked for merge compatibility.
""" """
@ -793,9 +768,7 @@ class StockMergeItemSerializer(serializers.Serializer):
class StockMergeSerializer(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: class Meta:
fields = [ fields = [
@ -879,8 +852,8 @@ class StockMergeSerializer(serializers.Serializer):
return data return data
def save(self): 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 At this point we are confident that the merge can take place
""" """
@ -908,8 +881,7 @@ class StockMergeSerializer(serializers.Serializer):
class StockAdjustmentItemSerializer(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: Fields:
- item: StockItem object - item: StockItem object
@ -940,9 +912,7 @@ class StockAdjustmentItemSerializer(serializers.Serializer):
class StockAdjustmentSerializer(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: class Meta:
fields = [ fields = [
@ -972,9 +942,7 @@ class StockAdjustmentSerializer(serializers.Serializer):
class StockCountSerializer(StockAdjustmentSerializer): class StockCountSerializer(StockAdjustmentSerializer):
""" """Serializer for counting stock items"""
Serializer for counting stock items
"""
def save(self): def save(self):
@ -998,9 +966,7 @@ class StockCountSerializer(StockAdjustmentSerializer):
class StockAddSerializer(StockAdjustmentSerializer): class StockAddSerializer(StockAdjustmentSerializer):
""" """Serializer for adding stock to stock item(s)"""
Serializer for adding stock to stock item(s)
"""
def save(self): def save(self):
@ -1023,9 +989,7 @@ class StockAddSerializer(StockAdjustmentSerializer):
class StockRemoveSerializer(StockAdjustmentSerializer): class StockRemoveSerializer(StockAdjustmentSerializer):
""" """Serializer for removing stock from stock item(s)"""
Serializer for removing stock from stock item(s)
"""
def save(self): def save(self):
@ -1048,9 +1012,7 @@ class StockRemoveSerializer(StockAdjustmentSerializer):
class StockTransferSerializer(StockAdjustmentSerializer): class StockTransferSerializer(StockAdjustmentSerializer):
""" """Serializer for transferring (moving) stock item(s)"""
Serializer for transferring (moving) stock item(s)
"""
location = serializers.PrimaryKeyRelatedField( location = serializers.PrimaryKeyRelatedField(
queryset=StockLocation.objects.all(), queryset=StockLocation.objects.all(),

View File

@ -1,6 +1,4 @@
""" """Unit testing for the Stock API"""
Unit testing for the Stock API
"""
import io import io
import os import os
@ -47,9 +45,7 @@ class StockAPITestCase(InvenTreeAPITestCase):
class StockLocationTest(StockAPITestCase): 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') list_url = reverse('api-location-list')
def setUp(self): def setUp(self):
@ -76,16 +72,12 @@ class StockLocationTest(StockAPITestCase):
class StockItemListTest(StockAPITestCase): class StockItemListTest(StockAPITestCase):
""" """Tests for the StockItem API LIST endpoint"""
Tests for the StockItem API LIST endpoint
"""
list_url = reverse('api-stock-list') list_url = reverse('api-stock-list')
def get_stock(self, **kwargs): 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) response = self.client.get(self.list_url, format='json', data=kwargs)
@ -95,18 +87,14 @@ class StockItemListTest(StockAPITestCase):
return response.data return response.data
def test_get_stock_list(self): def test_get_stock_list(self):
""" """List *all* StockItem objects."""
List *all* StockItem objects.
"""
response = self.get_stock() response = self.get_stock()
self.assertEqual(len(response), 29) self.assertEqual(len(response), 29)
def test_filter_by_part(self): def test_filter_by_part(self):
""" """Filter StockItem by Part reference"""
Filter StockItem by Part reference
"""
response = self.get_stock(part=25) response = self.get_stock(part=25)
@ -117,17 +105,13 @@ class StockItemListTest(StockAPITestCase):
self.assertEqual(len(response), 12) self.assertEqual(len(response), 12)
def test_filter_by_IPN(self): def test_filter_by_IPN(self):
""" """Filter StockItem by IPN reference"""
Filter StockItem by IPN reference
"""
response = self.get_stock(IPN="R.CH") response = self.get_stock(IPN="R.CH")
self.assertEqual(len(response), 3) self.assertEqual(len(response), 3)
def test_filter_by_location(self): def test_filter_by_location(self):
""" """Filter StockItem by StockLocation reference"""
Filter StockItem by StockLocation reference
"""
response = self.get_stock(location=5) response = self.get_stock(location=5)
self.assertEqual(len(response), 1) self.assertEqual(len(response), 1)
@ -142,9 +126,7 @@ class StockItemListTest(StockAPITestCase):
self.assertEqual(len(response), 18) self.assertEqual(len(response), 18)
def test_filter_by_depleted(self): def test_filter_by_depleted(self):
""" """Filter StockItem by depleted status"""
Filter StockItem by depleted status
"""
response = self.get_stock(depleted=1) response = self.get_stock(depleted=1)
self.assertEqual(len(response), 1) self.assertEqual(len(response), 1)
@ -153,9 +135,7 @@ class StockItemListTest(StockAPITestCase):
self.assertEqual(len(response), 28) self.assertEqual(len(response), 28)
def test_filter_by_in_stock(self): 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) response = self.get_stock(in_stock=1)
self.assertEqual(len(response), 26) self.assertEqual(len(response), 26)
@ -164,9 +144,7 @@ class StockItemListTest(StockAPITestCase):
self.assertEqual(len(response), 3) self.assertEqual(len(response), 3)
def test_filter_by_status(self): def test_filter_by_status(self):
""" """Filter StockItem by 'status' field"""
Filter StockItem by 'status' field
"""
codes = { codes = {
StockStatus.OK: 27, StockStatus.OK: 27,
@ -183,17 +161,13 @@ class StockItemListTest(StockAPITestCase):
self.assertEqual(len(response), num) self.assertEqual(len(response), num)
def test_filter_by_batch(self): def test_filter_by_batch(self):
""" """Filter StockItem by batch code"""
Filter StockItem by batch code
"""
response = self.get_stock(batch='B123') response = self.get_stock(batch='B123')
self.assertEqual(len(response), 1) self.assertEqual(len(response), 1)
def test_filter_by_serialized(self): def test_filter_by_serialized(self):
""" """Filter StockItem by serialized status"""
Filter StockItem by serialized status
"""
response = self.get_stock(serialized=1) response = self.get_stock(serialized=1)
self.assertEqual(len(response), 12) self.assertEqual(len(response), 12)
@ -208,9 +182,7 @@ class StockItemListTest(StockAPITestCase):
self.assertIsNone(item['serial']) self.assertIsNone(item['serial'])
def test_filter_by_has_batch(self): 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) with_batch = self.get_stock(has_batch=1)
without_batch = self.get_stock(has_batch=0) without_batch = self.get_stock(has_batch=0)
@ -227,8 +199,7 @@ class StockItemListTest(StockAPITestCase):
self.assertTrue(item['batch'] in [None, '']) self.assertTrue(item['batch'] in [None, ''])
def test_filter_by_tracked(self): 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 This checks if the stock item has either a batch code *or* a serial number
""" """
@ -248,9 +219,7 @@ class StockItemListTest(StockAPITestCase):
self.assertTrue(item['batch'] in blank and item['serial'] in blank) self.assertTrue(item['batch'] in blank and item['serial'] in blank)
def test_filter_by_expired(self): 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 # First, we can assume that the 'stock expiry' feature is disabled
response = self.get_stock(expired=1) response = self.get_stock(expired=1)
@ -289,9 +258,7 @@ class StockItemListTest(StockAPITestCase):
self.assertEqual(len(response), 25) self.assertEqual(len(response), 25)
def test_paginate(self): def test_paginate(self):
""" """Test that we can paginate results correctly"""
Test that we can paginate results correctly
"""
for n in [1, 5, 10]: for n in [1, 5, 10]:
response = self.get_stock(limit=n) response = self.get_stock(limit=n)
@ -321,9 +288,7 @@ class StockItemListTest(StockAPITestCase):
return dataset return dataset
def test_export(self): def test_export(self):
""" """Test exporting of Stock data via the API"""
Test exporting of Stock data via the API
"""
dataset = self.export_data({}) dataset = self.export_data({})
@ -361,9 +326,7 @@ class StockItemListTest(StockAPITestCase):
class StockItemTest(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') list_url = reverse('api-stock-list')
@ -376,8 +339,7 @@ class StockItemTest(StockAPITestCase):
StockLocation.objects.create(name='C', description='location c', parent=top) StockLocation.objects.create(name='C', description='location c', parent=top)
def test_create_default_location(self): def test_create_default_location(self):
""" """Test the default location functionality,
Test the default location functionality,
if a 'location' is not specified in the creation request. if a 'location' is not specified in the creation request.
""" """
@ -423,9 +385,7 @@ class StockItemTest(StockAPITestCase):
self.assertEqual(response.data['location'], None) self.assertEqual(response.data['location'], None)
def test_stock_item_create(self): 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 # POST with an empty part reference
@ -476,9 +436,7 @@ class StockItemTest(StockAPITestCase):
) )
def test_creation_with_serials(self): 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( trackable_part = part.models.Part.objects.create(
name='My part', name='My part',
@ -587,9 +545,7 @@ class StockItemTest(StockAPITestCase):
self.assertEqual(response.data['expiry_date'], expiry.isoformat()) self.assertEqual(response.data['expiry_date'], expiry.isoformat())
def test_purchase_price(self): 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}) url = reverse('api-stock-detail', kwargs={'pk': 1})
@ -648,7 +604,7 @@ class StockItemTest(StockAPITestCase):
self.assertEqual(data['purchase_price_currency'], 'NZD') self.assertEqual(data['purchase_price_currency'], 'NZD')
def test_install(self): 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 # Select the "parent" stock item
parent_part = part.models.Part.objects.get(pk=100) parent_part = part.models.Part.objects.get(pk=100)
@ -731,15 +687,10 @@ class StockItemTest(StockAPITestCase):
class StocktakeTest(StockAPITestCase): class StocktakeTest(StockAPITestCase):
""" """Series of tests for the Stocktake API"""
Series of tests for the Stocktake API
"""
def test_action(self): 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']: for endpoint in ['api-stock-count', 'api-stock-add', 'api-stock-remove']:
@ -796,9 +747,7 @@ class StocktakeTest(StockAPITestCase):
self.assertContains(response, 'Ensure this value is greater than or equal to 0', status_code=status.HTTP_400_BAD_REQUEST) self.assertContains(response, 'Ensure this value is greater than or equal to 0', status_code=status.HTTP_400_BAD_REQUEST)
def test_transfer(self): def test_transfer(self):
""" """Test stock transfers"""
Test stock transfers
"""
data = { data = {
'items': [ 'items': [
@ -825,9 +774,7 @@ class StocktakeTest(StockAPITestCase):
class StockItemDeletionTest(StockAPITestCase): class StockItemDeletionTest(StockAPITestCase):
""" """Tests for stock item deletion via the API"""
Tests for stock item deletion via the API
"""
def test_delete(self): def test_delete(self):
@ -974,8 +921,7 @@ class StockTestResultTest(StockAPITestCase):
class StockAssignTest(StockAPITestCase): class StockAssignTest(StockAPITestCase):
""" """Unit tests for the stock assignment API endpoint,
Unit tests for the stock assignment API endpoint,
where stock items are manually assigned to a customer where stock items are manually assigned to a customer
""" """
@ -1083,9 +1029,7 @@ class StockAssignTest(StockAPITestCase):
class StockMergeTest(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') URL = reverse('api-stock-merge')
@ -1117,9 +1061,7 @@ class StockMergeTest(StockAPITestCase):
) )
def test_missing_data(self): def test_missing_data(self):
""" """Test responses which are missing required data"""
Test responses which are missing required data
"""
# Post completely empty # Post completely empty
@ -1145,9 +1087,7 @@ class StockMergeTest(StockAPITestCase):
self.assertIn('At least two stock items', str(data)) self.assertIn('At least two stock items', str(data))
def test_invalid_data(self): def test_invalid_data(self):
""" """Test responses which have invalid data"""
Test responses which have invalid data
"""
# Serialized stock items should be rejected # Serialized stock items should be rejected
data = self.post( data = self.post(
@ -1229,9 +1169,7 @@ class StockMergeTest(StockAPITestCase):
self.assertIn('Stock items must refer to the same supplier part', str(data)) self.assertIn('Stock items must refer to the same supplier part', str(data))
def test_valid_merge(self): def test_valid_merge(self):
""" """Test valid merging of stock items"""
Test valid merging of stock items
"""
# Check initial conditions # Check initial conditions
n = StockItem.objects.filter(part=self.part).count() n = StockItem.objects.filter(part=self.part).count()

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 from django.urls import reverse
@ -22,7 +22,7 @@ class StockViewTestCase(InvenTreeTestCase):
class StockListTest(StockViewTestCase): class StockListTest(StockViewTestCase):
""" Tests for Stock list views """ """Tests for Stock list views"""
def test_stock_index(self): def test_stock_index(self):
response = self.client.get(reverse('stock-index')) response = self.client.get(reverse('stock-index'))
@ -30,10 +30,10 @@ class StockListTest(StockViewTestCase):
class StockOwnershipTest(StockViewTestCase): class StockOwnershipTest(StockViewTestCase):
""" Tests for stock ownership views """ """Tests for stock ownership views"""
def setUp(self): 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 TODO: Refactor this following test to use the new API form

View File

@ -13,9 +13,7 @@ from .models import (StockItem, StockItemTestResult, StockItemTracking,
class StockTest(InvenTreeTestCase): class StockTest(InvenTreeTestCase):
""" """Tests to ensure that the stock location tree functions correcly"""
Tests to ensure that the stock location tree functions correcly
"""
fixtures = [ fixtures = [
'category', 'category',
@ -44,10 +42,7 @@ class StockTest(InvenTreeTestCase):
StockItem.objects.rebuild() StockItem.objects.rebuild()
def test_expiry(self): def test_expiry(self):
""" """Test expiry date functionality for StockItem model."""
Test expiry date functionality for StockItem model.
"""
today = datetime.datetime.now().date() today = datetime.datetime.now().date()
item = StockItem.objects.create( item = StockItem.objects.create(
@ -78,10 +73,7 @@ class StockTest(InvenTreeTestCase):
self.assertTrue(item.is_expired()) self.assertTrue(item.is_expired())
def test_is_building(self): 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) part = Part.objects.get(pk=1)
# Record the total stock count # Record the total stock count
@ -197,7 +189,6 @@ class StockTest(InvenTreeTestCase):
def test_move(self): def test_move(self):
""" Test stock movement functions """ """ Test stock movement functions """
# Move 4,000 screws to the bathroom # Move 4,000 screws to the bathroom
it = StockItem.objects.get(pk=1) it = StockItem.objects.get(pk=1)
self.assertNotEqual(it.location, self.bathroom) self.assertNotEqual(it.location, self.bathroom)
@ -339,10 +330,7 @@ class StockTest(InvenTreeTestCase):
w2 = StockItem.objects.get(pk=101) w2 = StockItem.objects.get(pk=101)
def test_serials(self): def test_serials(self):
""" """Tests for stock serialization"""
Tests for stock serialization
"""
p = Part.objects.create( p = Part.objects.create(
name='trackable part', name='trackable part',
description='trackable part', description='trackable part',
@ -373,10 +361,7 @@ class StockTest(InvenTreeTestCase):
self.assertTrue(item.serialized) self.assertTrue(item.serialized)
def test_big_serials(self): 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( p = Part.objects.create(
name='trackable part', name='trackable part',
description='trackable part', description='trackable part',
@ -451,11 +436,10 @@ class StockTest(InvenTreeTestCase):
self.assertEqual(item_prev.serial_int, 99) self.assertEqual(item_prev.serial_int, 99)
def test_serialize_stock_invalid(self): def test_serialize_stock_invalid(self):
""" """Test manual serialization of parts.
Test manual serialization of parts.
Each of these tests should fail Each of these tests should fail
""" """
# Test serialization of non-serializable part # Test serialization of non-serializable part
item = StockItem.objects.get(pk=1234) item = StockItem.objects.get(pk=1234)
@ -480,8 +464,7 @@ class StockTest(InvenTreeTestCase):
item.serializeStock(3, "hello", self.user) item.serializeStock(3, "hello", self.user)
def test_serialize_stock_valid(self): def test_serialize_stock_valid(self):
""" Perform valid stock serializations """ """Perform valid stock serializations"""
# There are 10 of these in stock # There are 10 of these in stock
# Item will deplete when deleted # Item will deplete when deleted
item = StockItem.objects.get(pk=100) item = StockItem.objects.get(pk=100)
@ -517,8 +500,8 @@ class StockTest(InvenTreeTestCase):
item.serializeStock(2, [99, 100], self.user) item.serializeStock(2, [99, 100], self.user)
def test_location_tree(self): 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, Ensure that the MPTT structure is rebuilt correctly,
and the corrent ancestor tree is observed. and the corrent ancestor tree is observed.
@ -686,9 +669,7 @@ class StockTest(InvenTreeTestCase):
class VariantTest(StockTest): class VariantTest(StockTest):
""" """Tests for calculation stock counts against templates / variants"""
Tests for calculation stock counts against templates / variants
"""
def test_variant_stock(self): def test_variant_stock(self):
# Check the 'Chair' variant # Check the 'Chair' variant
@ -769,9 +750,7 @@ class VariantTest(StockTest):
class TestResultTest(StockTest): class TestResultTest(StockTest):
""" """Tests for the StockItemTestResult model."""
Tests for the StockItemTestResult model.
"""
def test_test_count(self): def test_test_count(self):
item = StockItem.objects.get(pk=105) item = StockItem.objects.get(pk=105)
@ -898,12 +877,10 @@ class TestResultTest(StockTest):
self.assertEqual(item3.test_results.count(), 4) self.assertEqual(item3.test_results.count(), 4)
def test_installed_tests(self): 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" Or, test "test results" for "stock items" installed "inside" a "stock item"
""" """
# Get a "master" stock item # Get a "master" stock item
item = StockItem.objects.get(pk=105) 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 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 from datetime import datetime
@ -21,8 +19,7 @@ from .models import StockItem, StockItemTracking, StockLocation
class StockIndex(InvenTreeRoleMixin, InvenTreePluginViewMixin, ListView): class StockIndex(InvenTreeRoleMixin, InvenTreePluginViewMixin, ListView):
""" StockIndex view loads all StockLocation and StockItem object """StockIndex view loads all StockLocation and StockItem object"""
"""
model = StockItem model = StockItem
template_name = 'stock/location.html' template_name = 'stock/location.html'
context_obect_name = 'locations' context_obect_name = 'locations'
@ -48,9 +45,7 @@ class StockIndex(InvenTreeRoleMixin, InvenTreePluginViewMixin, ListView):
class StockLocationDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView): class StockLocationDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
""" """Detailed view of a single StockLocation object"""
Detailed view of a single StockLocation object
"""
context_object_name = 'location' context_object_name = 'location'
template_name = 'stock/location.html' template_name = 'stock/location.html'
@ -69,9 +64,7 @@ class StockLocationDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailVi
class StockItemDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView): class StockItemDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
""" """Detailed view of a single StockItem object"""
Detailed view of a single StockItem object
"""
context_object_name = 'item' context_object_name = 'item'
template_name = 'stock/item.html' template_name = 'stock/item.html'
@ -103,7 +96,7 @@ class StockItemDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
return data return data
def get(self, request, *args, **kwargs): 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) stock_pk = kwargs.get('pk', None)
@ -120,7 +113,7 @@ class StockItemDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
class StockLocationQRCode(QRCodeView): 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") ajax_form_title = _("Stock Location QR code")
@ -136,9 +129,7 @@ class StockLocationQRCode(QRCodeView):
class StockItemReturnToStock(AjaxUpdateView): 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 model = StockItem
ajax_form_title = _("Return to Stock") ajax_form_title = _("Return to Stock")
@ -166,9 +157,7 @@ class StockItemReturnToStock(AjaxUpdateView):
class StockItemDeleteTestData(AjaxUpdateView): class StockItemDeleteTestData(AjaxUpdateView):
""" """View for deleting all test data"""
View for deleting all test data
"""
model = StockItem model = StockItem
form_class = ConfirmForm form_class = ConfirmForm
@ -203,7 +192,7 @@ class StockItemDeleteTestData(AjaxUpdateView):
class StockItemQRCode(QRCodeView): 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") ajax_form_title = _("Stock Item QR Code")
role_required = 'stock.view' role_required = 'stock.view'
@ -218,9 +207,7 @@ class StockItemQRCode(QRCodeView):
class StockItemConvert(AjaxUpdateView): 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 model = StockItem
form_class = StockForms.ConvertStockItemForm form_class = StockForms.ConvertStockItemForm
@ -229,9 +216,7 @@ class StockItemConvert(AjaxUpdateView):
context_object_name = 'item' context_object_name = 'item'
def get_form(self): def get_form(self):
""" """Filter the available parts."""
Filter the available parts.
"""
form = super().get_form() form = super().get_form()
item = self.get_object() item = self.get_object()
@ -289,7 +274,7 @@ class StockItemTrackingDelete(AjaxDeleteView):
class StockItemTrackingEdit(AjaxUpdateView): class StockItemTrackingEdit(AjaxUpdateView):
""" View for editing a StockItemTracking object """ """View for editing a StockItemTracking object"""
model = StockItemTracking model = StockItemTracking
ajax_form_title = _('Edit Stock Tracking Entry') ajax_form_title = _('Edit Stock Tracking Entry')
@ -297,8 +282,7 @@ class StockItemTrackingEdit(AjaxUpdateView):
class StockItemTrackingCreate(AjaxCreateView): class StockItemTrackingCreate(AjaxCreateView):
""" View for creating a new StockItemTracking object. """View for creating a new StockItemTracking object."""
"""
model = StockItemTracking model = StockItemTracking
ajax_form_title = _("Add Stock Tracking Entry") ajax_form_title = _("Add Stock Tracking Entry")

View File

@ -14,9 +14,7 @@ User = get_user_model()
class RuleSetInline(admin.TabularInline): class RuleSetInline(admin.TabularInline):
""" """Class for displaying inline RuleSet data in the Group admin page."""
Class for displaying inline RuleSet data in the Group admin page.
"""
model = RuleSet model = RuleSet
can_delete = False can_delete = False
@ -76,9 +74,7 @@ class InvenTreeGroupAdminForm(forms.ModelForm):
class RoleGroupAdmin(admin.ModelAdmin): # pragma: no cover class RoleGroupAdmin(admin.ModelAdmin): # pragma: no cover
""" """Custom admin interface for the Group model"""
Custom admin interface for the Group model
"""
form = InvenTreeGroupAdminForm form = InvenTreeGroupAdminForm
@ -213,9 +209,7 @@ class InvenTreeUserAdmin(UserAdmin):
class OwnerAdmin(admin.ModelAdmin): class OwnerAdmin(admin.ModelAdmin):
""" """Custom admin interface for the Owner model"""
Custom admin interface for the Owner model
"""
pass pass

View File

@ -14,9 +14,7 @@ from users.serializers import OwnerSerializer, UserSerializer
class OwnerList(generics.ListAPIView): class OwnerList(generics.ListAPIView):
""" """List API endpoint for Owner model. Cannot create."""
List API endpoint for Owner model. Cannot create.
"""
queryset = Owner.objects.all() queryset = Owner.objects.all()
serializer_class = OwnerSerializer serializer_class = OwnerSerializer
@ -54,9 +52,7 @@ class OwnerList(generics.ListAPIView):
class OwnerDetail(generics.RetrieveAPIView): class OwnerDetail(generics.RetrieveAPIView):
""" """Detail API endpoint for Owner model. Cannot edit or delete"""
Detail API endpoint for Owner model. Cannot edit or delete
"""
queryset = Owner.objects.all() queryset = Owner.objects.all()
serializer_class = OwnerSerializer serializer_class = OwnerSerializer
@ -108,7 +104,7 @@ class RoleDetails(APIView):
class UserDetail(generics.RetrieveAPIView): class UserDetail(generics.RetrieveAPIView):
""" Detail endpoint for a single user """ """Detail endpoint for a single user"""
queryset = User.objects.all() queryset = User.objects.all()
serializer_class = UserSerializer serializer_class = UserSerializer
@ -116,7 +112,7 @@ class UserDetail(generics.RetrieveAPIView):
class UserList(generics.ListAPIView): class UserList(generics.ListAPIView):
""" List endpoint for detail on all users """ """List endpoint for detail on all users"""
queryset = User.objects.all() queryset = User.objects.all()
serializer_class = UserSerializer serializer_class = UserSerializer
@ -135,7 +131,7 @@ class UserList(generics.ListAPIView):
class GetAuthToken(APIView): class GetAuthToken(APIView):
""" Return authentication token for an authenticated user. """ """Return authentication token for an authenticated user."""
permission_classes = [ permission_classes = [
permissions.IsAuthenticated, permissions.IsAuthenticated,

View File

@ -221,9 +221,7 @@ class RuleSet(models.Model):
@classmethod @classmethod
def check_table_permission(cls, user, table, permission): def check_table_permission(cls, user, table, permission):
""" """Check if the provided user has the specified permission against the table"""
Check if the provided user has the specified permission against the table
"""
# If the table does *not* require permissions # If the table does *not* require permissions
if table in cls.RULESET_IGNORE: if table in cls.RULESET_IGNORE:
@ -269,7 +267,7 @@ class RuleSet(models.Model):
) )
def __str__(self, debug=False): # pragma: no cover def __str__(self, debug=False): # pragma: no cover
""" Ruleset string representation """ """Ruleset string representation"""
if debug: if debug:
# Makes debugging easier # Makes debugging easier
return f'{str(self.group).ljust(15)}: {self.name.title().ljust(15)} | ' \ return f'{str(self.group).ljust(15)}: {self.name.title().ljust(15)} | ' \
@ -296,15 +294,13 @@ class RuleSet(models.Model):
self.group.save() self.group.save()
def get_models(self): def get_models(self):
""" """Return the database tables / models that this ruleset covers."""
Return the database tables / models that this ruleset covers.
"""
return self.RULESET_MODELS.get(self.name, []) return self.RULESET_MODELS.get(self.name, [])
def split_model(model): def split_model(model):
"""get modelname and app from modelstring""" """Get modelname and app from modelstring"""
*app, model = model.split('_') *app, model = model.split('_')
# handle models that have # handle models that have
@ -317,7 +313,7 @@ def split_model(model):
def split_permission(app, perm): def split_permission(app, perm):
"""split permission string into permission and model""" """Split permission string into permission and model"""
permission_name, *model = perm.split('_') permission_name, *model = perm.split('_')
# handle models that have underscores # handle models that have underscores
if len(model) > 1: # pragma: no cover if len(model) > 1: # pragma: no cover
@ -329,7 +325,6 @@ def split_permission(app, perm):
def update_group_roles(group, debug=False): def update_group_roles(group, debug=False):
""" """
Iterates through all of the RuleSets associated with the group, Iterates through all of the RuleSets associated with the group,
and ensures that the correct permissions are either applied or removed from the group. and ensures that the correct permissions are either applied or removed from the group.
@ -339,7 +334,6 @@ def update_group_roles(group, debug=False):
b) Whenver the group object is updated b) Whenver the group object is updated
The RuleSet model has complete control over the permissions applied to any group. The RuleSet model has complete control over the permissions applied to any group.
""" """
if not canAppAccessDatabase(allow_test=True): if not canAppAccessDatabase(allow_test=True):
@ -594,24 +588,20 @@ class Owner(models.Model):
owner = GenericForeignKey('owner_type', 'owner_id') owner = GenericForeignKey('owner_type', 'owner_id')
def __str__(self): def __str__(self):
""" Defines the owner string representation """ """Defines the owner string representation"""
return f'{self.owner} ({self.owner_type.name})' return f'{self.owner} ({self.owner_type.name})'
def name(self): def name(self):
""" """Return the 'name' of this owner"""
Return the 'name' of this owner
"""
return str(self.owner) return str(self.owner)
def label(self): def label(self):
""" """Return the 'type' label of this owner i.e. 'user' or 'group'"""
Return the 'type' label of this owner i.e. 'user' or 'group'
"""
return str(self.owner_type.name) return str(self.owner_type.name)
@classmethod @classmethod
def create(cls, obj): def create(cls, obj):
""" Check if owner exist then create new owner entry """ """Check if owner exist then create new owner entry"""
# Check for existing owner # Check for existing owner
existing_owner = cls.get_owner(obj) existing_owner = cls.get_owner(obj)
@ -627,7 +617,7 @@ class Owner(models.Model):
@classmethod @classmethod
def get_owner(cls, user_or_group): def get_owner(cls, user_or_group):
""" Get owner instance for a group or user """ """Get owner instance for a group or user"""
user_model = get_user_model() user_model = get_user_model()
owner = None owner = None

View File

@ -10,8 +10,7 @@ from .models import Owner
class UserSerializer(InvenTreeModelSerializer): class UserSerializer(InvenTreeModelSerializer):
""" Serializer for a User """Serializer for a User"""
"""
class Meta: class Meta:
model = User model = User
@ -23,9 +22,7 @@ class UserSerializer(InvenTreeModelSerializer):
class OwnerSerializer(InvenTreeModelSerializer): class OwnerSerializer(InvenTreeModelSerializer):
""" """Serializer for an "Owner" (either a "user" or a "group")"""
Serializer for an "Owner" (either a "user" or a "group")
"""
name = serializers.CharField(read_only=True) name = serializers.CharField(read_only=True)

View File

@ -1,6 +1,4 @@
""" """Unit tests for the user model database migrations"""
Unit tests for the user model database migrations
"""
from django_test_migrations.contrib.unittest_case import MigratorTestCase from django_test_migrations.contrib.unittest_case import MigratorTestCase
@ -8,9 +6,7 @@ from InvenTree import helpers
class TestForwardMigrations(MigratorTestCase): class TestForwardMigrations(MigratorTestCase):
""" """Test entire schema migration sequence for the users app"""
Test entire schema migration sequence for the users app
"""
migrate_from = ('users', helpers.getOldestMigrationFile('users')) migrate_from = ('users', helpers.getOldestMigrationFile('users'))
migrate_to = ('users', helpers.getNewestMigrationFile('users')) migrate_to = ('users', helpers.getNewestMigrationFile('users'))

View File

@ -10,9 +10,7 @@ from users.models import Owner, RuleSet
class RuleSetModelTest(TestCase): class RuleSetModelTest(TestCase):
""" """Some simplistic tests to ensure the RuleSet model is setup correctly."""
Some simplistic tests to ensure the RuleSet model is setup correctly.
"""
def test_ruleset_models(self): def test_ruleset_models(self):
@ -48,10 +46,7 @@ class RuleSetModelTest(TestCase):
self.assertEqual(len(empty), 0) self.assertEqual(len(empty), 0)
def test_model_names(self): def test_model_names(self):
""" """Test that each model defined in the rulesets is valid, based on the database schema!"""
Test that each model defined in the rulesets is valid,
based on the database schema!
"""
available_models = apps.get_models() available_models = apps.get_models()
@ -108,9 +103,7 @@ class RuleSetModelTest(TestCase):
self.assertEqual(len(extra_models), 0) self.assertEqual(len(extra_models), 0)
def test_permission_assign(self): def test_permission_assign(self):
""" """Test that the permission assigning works!"""
Test that the permission assigning works!
"""
# Create a new group # Create a new group
group = Group.objects.create(name="Test group") group = Group.objects.create(name="Test group")
@ -161,9 +154,7 @@ class RuleSetModelTest(TestCase):
class OwnerModelTest(InvenTreeTestCase): class OwnerModelTest(InvenTreeTestCase):
""" """Some simplistic tests to ensure the Owner model is setup correctly."""
Some simplistic tests to ensure the Owner model is setup correctly.
"""
def do_request(self, endpoint, filters, status_code=200): def do_request(self, endpoint, filters, status_code=200):
response = self.client.get(endpoint, filters, format='json') response = self.client.get(endpoint, filters, format='json')
@ -212,9 +203,7 @@ class OwnerModelTest(InvenTreeTestCase):
self.assertEqual(group_as_owner, None) self.assertEqual(group_as_owner, None)
def test_api(self): def test_api(self):
""" """Test user APIs"""
Test user APIs
"""
self.client.logout() self.client.logout()
# not authed # not authed
@ -231,9 +220,7 @@ class OwnerModelTest(InvenTreeTestCase):
# self.do_request(reverse('api-owner-detail', kwargs={'pk': self.user.id}), {}) # self.do_request(reverse('api-owner-detail', kwargs={'pk': self.user.id}), {})
def test_token(self): def test_token(self):
""" """Test token mechanisms"""
Test token mechanisms
"""
self.client.logout() self.client.logout()
token = Token.objects.filter(user=self.user) token = Token.objects.filter(user=self.user)