mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
reformat comments 1
This commit is contained in:
parent
959e4bb28e
commit
9b40fddf7c
@ -567,8 +567,7 @@ def rename_asset(instance, filename):
|
||||
|
||||
|
||||
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.
|
||||
Uploaded asset files appear in MEDIA_ROOT/report/assets,
|
||||
and can be loaded in a template using the {% report_asset <filename> %} tag.
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
Template tags for rendering various barcodes
|
||||
"""
|
||||
"""Template tags for rendering various barcodes"""
|
||||
|
||||
import base64
|
||||
from io import BytesIO
|
||||
@ -14,12 +12,10 @@ register = template.Library()
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
buffered = BytesIO()
|
||||
img.save(buffered, format=fmt)
|
||||
|
||||
@ -30,8 +26,7 @@ def image_data(img, fmt='PNG'):
|
||||
|
||||
@register.simple_tag()
|
||||
def qrcode(data, **kwargs):
|
||||
"""
|
||||
Return a byte-encoded QR code image
|
||||
"""Return a byte-encoded QR code image
|
||||
|
||||
Optional kwargs
|
||||
---------------
|
||||
@ -39,7 +34,6 @@ def qrcode(data, **kwargs):
|
||||
fill_color: Fill color (default = black)
|
||||
back_color: Background color (default = white)
|
||||
"""
|
||||
|
||||
# Construct "default" values
|
||||
params = dict(
|
||||
box_size=20,
|
||||
@ -63,10 +57,7 @@ def qrcode(data, **kwargs):
|
||||
|
||||
@register.simple_tag()
|
||||
def barcode(data, barcode_class='code128', **kwargs):
|
||||
"""
|
||||
Render a barcode
|
||||
"""
|
||||
|
||||
"""Render a barcode"""
|
||||
constructor = python_barcode.get_barcode_class(barcode_class)
|
||||
|
||||
data = str(data).zfill(constructor.digits)
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
Custom template tags for report generation
|
||||
"""
|
||||
"""Custom template tags for report generation"""
|
||||
|
||||
import os
|
||||
|
||||
@ -19,10 +17,7 @@ register = template.Library()
|
||||
|
||||
@register.simple_tag()
|
||||
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
|
||||
debug_mode = InvenTreeSetting.get_setting('REPORT_DEBUG_MODE')
|
||||
|
||||
@ -38,10 +33,7 @@ def asset(filename):
|
||||
|
||||
@register.simple_tag()
|
||||
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
|
||||
debug_mode = InvenTreeSetting.get_setting('REPORT_DEBUG_MODE')
|
||||
|
||||
@ -75,10 +67,7 @@ def part_image(part):
|
||||
|
||||
@register.simple_tag()
|
||||
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
|
||||
debug_mode = InvenTreeSetting.get_setting('REPORT_DEBUG_MODE')
|
||||
|
||||
@ -108,15 +97,13 @@ def company_image(company):
|
||||
|
||||
@register.simple_tag()
|
||||
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!
|
||||
|
||||
If the INVENTREE_BASE_URL parameter is not configured,
|
||||
the text will be returned (unlinked)
|
||||
"""
|
||||
|
||||
text = str(text)
|
||||
|
||||
url = InvenTree.helpers.construct_absolute_url(link)
|
||||
|
@ -36,10 +36,7 @@ class ReportTest(InvenTreeAPITestCase):
|
||||
super().setUp()
|
||||
|
||||
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(
|
||||
os.path.dirname(os.path.realpath(__file__)),
|
||||
'templates',
|
||||
@ -81,10 +78,7 @@ class ReportTest(InvenTreeAPITestCase):
|
||||
)
|
||||
|
||||
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:
|
||||
return
|
||||
|
||||
@ -135,10 +129,7 @@ class TestReportTest(ReportTest):
|
||||
return super().setUp()
|
||||
|
||||
def test_print(self):
|
||||
"""
|
||||
Printing tests for the TestReport
|
||||
"""
|
||||
|
||||
"""Printing tests for the TestReport"""
|
||||
report = self.model.objects.first()
|
||||
|
||||
url = reverse(self.print_url, kwargs={'pk': report.pk})
|
||||
@ -177,10 +168,7 @@ class BuildReportTest(ReportTest):
|
||||
return super().setUp()
|
||||
|
||||
def test_print(self):
|
||||
"""
|
||||
Printing tests for the BuildReport
|
||||
"""
|
||||
|
||||
"""Printing tests for the BuildReport"""
|
||||
report = self.model.objects.first()
|
||||
|
||||
url = reverse(self.print_url, kwargs={'pk': report.pk})
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
This script calculates translation coverage for various languages
|
||||
"""
|
||||
"""This script calculates translation coverage for various languages"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
@ -1,5 +1,4 @@
|
||||
"""
|
||||
The Stock module is responsible for Stock management.
|
||||
"""The Stock module is responsible for Stock management.
|
||||
|
||||
It includes models for:
|
||||
|
||||
|
@ -15,7 +15,7 @@ from .models import (StockItem, StockItemAttachment, StockItemTestResult,
|
||||
|
||||
|
||||
class LocationResource(ModelResource):
|
||||
""" Class for managing StockLocation data import/export """
|
||||
"""Class for managing StockLocation data import/export"""
|
||||
|
||||
parent = Field(attribute='parent', widget=widgets.ForeignKeyWidget(StockLocation))
|
||||
|
||||
@ -42,9 +42,7 @@ class LocationResource(ModelResource):
|
||||
|
||||
|
||||
class LocationInline(admin.TabularInline):
|
||||
"""
|
||||
Inline for sub-locations
|
||||
"""
|
||||
"""Inline for sub-locations"""
|
||||
model = StockLocation
|
||||
|
||||
|
||||
@ -66,7 +64,7 @@ class LocationAdmin(ImportExportModelAdmin):
|
||||
|
||||
|
||||
class StockItemResource(ModelResource):
|
||||
""" Class for managing StockItem data import/export """
|
||||
"""Class for managing StockItem data import/export"""
|
||||
|
||||
# Custom managers for ForeignKey fields
|
||||
part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part))
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
JSON API for the Stock app
|
||||
"""
|
||||
"""JSON API for the Stock app"""
|
||||
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime, timedelta
|
||||
@ -39,7 +37,7 @@ from stock.models import (StockItem, StockItemAttachment, StockItemTestResult,
|
||||
|
||||
|
||||
class StockDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
""" API detail endpoint for Stock object
|
||||
"""API detail endpoint for Stock object
|
||||
|
||||
get:
|
||||
Return a single StockItem object
|
||||
@ -89,7 +87,7 @@ class StockMetadata(generics.RetrieveUpdateAPIView):
|
||||
|
||||
|
||||
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):
|
||||
|
||||
@ -105,17 +103,14 @@ class StockItemContextMixin:
|
||||
|
||||
|
||||
class StockItemSerialize(StockItemContextMixin, generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint for serializing a stock item
|
||||
"""
|
||||
"""API endpoint for serializing a stock item"""
|
||||
|
||||
queryset = StockItem.objects.none()
|
||||
serializer_class = StockSerializers.SerializeStockItemSerializer
|
||||
|
||||
|
||||
class StockItemInstall(StockItemContextMixin, generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint for installing a particular stock item into this stock item.
|
||||
"""API endpoint for installing a particular stock item into this stock item.
|
||||
|
||||
- stock_item.part must be in the BOM for this part
|
||||
- stock_item must currently be "in stock"
|
||||
@ -127,17 +122,14 @@ class StockItemInstall(StockItemContextMixin, generics.CreateAPIView):
|
||||
|
||||
|
||||
class StockItemUninstall(StockItemContextMixin, generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint for removing (uninstalling) items from this item
|
||||
"""
|
||||
"""API endpoint for removing (uninstalling) items from this item"""
|
||||
|
||||
queryset = StockItem.objects.none()
|
||||
serializer_class = StockSerializers.UninstallStockItemSerializer
|
||||
|
||||
|
||||
class StockAdjustView(generics.CreateAPIView):
|
||||
"""
|
||||
A generic class for handling stocktake actions.
|
||||
"""A generic class for handling stocktake actions.
|
||||
|
||||
Subclasses exist for:
|
||||
|
||||
@ -159,41 +151,31 @@ class StockAdjustView(generics.CreateAPIView):
|
||||
|
||||
|
||||
class StockCount(StockAdjustView):
|
||||
"""
|
||||
Endpoint for counting stock (performing a stocktake).
|
||||
"""
|
||||
"""Endpoint for counting stock (performing a stocktake)."""
|
||||
|
||||
serializer_class = StockSerializers.StockCountSerializer
|
||||
|
||||
|
||||
class StockAdd(StockAdjustView):
|
||||
"""
|
||||
Endpoint for adding a quantity of stock to an existing StockItem
|
||||
"""
|
||||
"""Endpoint for adding a quantity of stock to an existing StockItem"""
|
||||
|
||||
serializer_class = StockSerializers.StockAddSerializer
|
||||
|
||||
|
||||
class StockRemove(StockAdjustView):
|
||||
"""
|
||||
Endpoint for removing a quantity of stock from an existing StockItem.
|
||||
"""
|
||||
"""Endpoint for removing a quantity of stock from an existing StockItem."""
|
||||
|
||||
serializer_class = StockSerializers.StockRemoveSerializer
|
||||
|
||||
|
||||
class StockTransfer(StockAdjustView):
|
||||
"""
|
||||
API endpoint for performing stock movements
|
||||
"""
|
||||
"""API endpoint for performing stock movements"""
|
||||
|
||||
serializer_class = StockSerializers.StockTransferSerializer
|
||||
|
||||
|
||||
class StockAssign(generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint for assigning stock to a particular customer
|
||||
"""
|
||||
"""API endpoint for assigning stock to a particular customer"""
|
||||
|
||||
queryset = StockItem.objects.all()
|
||||
serializer_class = StockSerializers.StockAssignmentSerializer
|
||||
@ -208,9 +190,7 @@ class StockAssign(generics.CreateAPIView):
|
||||
|
||||
|
||||
class StockMerge(generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint for merging multiple stock items
|
||||
"""
|
||||
"""API endpoint for merging multiple stock items"""
|
||||
|
||||
queryset = StockItem.objects.none()
|
||||
serializer_class = StockSerializers.StockMergeSerializer
|
||||
@ -222,8 +202,7 @@ class StockMerge(generics.CreateAPIView):
|
||||
|
||||
|
||||
class StockLocationList(generics.ListCreateAPIView):
|
||||
"""
|
||||
API endpoint for list view of StockLocation objects:
|
||||
"""API endpoint for list view of StockLocation objects:
|
||||
|
||||
- GET: Return list of StockLocation objects
|
||||
- POST: Create a new StockLocation
|
||||
@ -233,11 +212,9 @@ class StockLocationList(generics.ListCreateAPIView):
|
||||
serializer_class = StockSerializers.LocationSerializer
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
"""
|
||||
Custom filtering:
|
||||
"""Custom filtering:
|
||||
- Allow filtering by "null" parent to retrieve top-level stock locations
|
||||
"""
|
||||
|
||||
queryset = super().filter_queryset(queryset)
|
||||
|
||||
params = self.request.query_params
|
||||
@ -319,10 +296,7 @@ class StockLocationList(generics.ListCreateAPIView):
|
||||
|
||||
|
||||
class StockLocationTree(generics.ListAPIView):
|
||||
"""
|
||||
API endpoint for accessing a list of StockLocation objects,
|
||||
ready for rendering as a tree
|
||||
"""
|
||||
"""API endpoint for accessing a list of StockLocation objects, ready for rendering as a tree"""
|
||||
|
||||
queryset = StockLocation.objects.all()
|
||||
serializer_class = StockSerializers.LocationTreeSerializer
|
||||
@ -337,9 +311,7 @@ class StockLocationTree(generics.ListAPIView):
|
||||
|
||||
|
||||
class StockFilter(rest_filters.FilterSet):
|
||||
"""
|
||||
FilterSet for StockItem LIST API
|
||||
"""
|
||||
"""FilterSet for StockItem LIST API"""
|
||||
|
||||
# Part name filters
|
||||
name = rest_filters.CharFilter(label='Part name (case insensitive)', field_name='part__name', lookup_expr='iexact')
|
||||
@ -372,12 +344,10 @@ class StockFilter(rest_filters.FilterSet):
|
||||
available = rest_filters.BooleanFilter(label='Available', method='filter_available')
|
||||
|
||||
def filter_available(self, queryset, name, value):
|
||||
"""
|
||||
Filter by whether the StockItem is "available" or not.
|
||||
"""Filter by whether the StockItem is "available" or not.
|
||||
|
||||
Here, "available" means that the allocated quantity is less than the total quantity
|
||||
"""
|
||||
|
||||
if str2bool(value):
|
||||
# The 'quantity' field is greater than the calculated 'allocated' field
|
||||
queryset = queryset.filter(Q(quantity__gt=F('allocated')))
|
||||
@ -401,10 +371,7 @@ class StockFilter(rest_filters.FilterSet):
|
||||
serialized = rest_filters.BooleanFilter(label='Has serial number', method='filter_serialized')
|
||||
|
||||
def filter_serialized(self, queryset, name, value):
|
||||
"""
|
||||
Filter by whether the StockItem has a serial number (or not)
|
||||
"""
|
||||
|
||||
"""Filter by whether the StockItem has a serial number (or not)"""
|
||||
q = Q(serial=None) | Q(serial='')
|
||||
|
||||
if str2bool(value):
|
||||
@ -417,10 +384,7 @@ class StockFilter(rest_filters.FilterSet):
|
||||
has_batch = rest_filters.BooleanFilter(label='Has batch code', method='filter_has_batch')
|
||||
|
||||
def filter_has_batch(self, queryset, name, value):
|
||||
"""
|
||||
Filter by whether the StockItem has a batch code (or not)
|
||||
"""
|
||||
|
||||
"""Filter by whether the StockItem has a batch code (or not)"""
|
||||
q = Q(batch=None) | Q(batch='')
|
||||
|
||||
if str2bool(value):
|
||||
@ -433,12 +397,10 @@ class StockFilter(rest_filters.FilterSet):
|
||||
tracked = rest_filters.BooleanFilter(label='Tracked', method='filter_tracked')
|
||||
|
||||
def filter_tracked(self, queryset, name, value):
|
||||
"""
|
||||
Filter by whether this stock item is *tracked*, meaning either:
|
||||
"""Filter by whether this stock item is *tracked*, meaning either:
|
||||
- It has a serial number
|
||||
- It has a batch code
|
||||
"""
|
||||
|
||||
q_batch = Q(batch=None) | Q(batch='')
|
||||
q_serial = Q(serial=None) | Q(serial='')
|
||||
|
||||
@ -452,10 +414,7 @@ class StockFilter(rest_filters.FilterSet):
|
||||
installed = rest_filters.BooleanFilter(label='Installed in other stock item', method='filter_installed')
|
||||
|
||||
def filter_installed(self, queryset, name, value):
|
||||
"""
|
||||
Filter stock items by "belongs_to" field being empty
|
||||
"""
|
||||
|
||||
"""Filter stock items by "belongs_to" field being empty"""
|
||||
if str2bool(value):
|
||||
queryset = queryset.exclude(belongs_to=None)
|
||||
else:
|
||||
@ -502,7 +461,7 @@ class StockFilter(rest_filters.FilterSet):
|
||||
|
||||
|
||||
class StockList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
""" API endpoint for list view of Stock objects
|
||||
"""API endpoint for list view of Stock objects
|
||||
|
||||
- GET: Return a list of all StockItem objects (with optional query filters)
|
||||
- POST: Create a new StockItem
|
||||
@ -520,15 +479,13 @@ class StockList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
return ctx
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""
|
||||
Create a new StockItem object via the API.
|
||||
"""Create a new StockItem object via the API.
|
||||
|
||||
We override the default 'create' implementation.
|
||||
|
||||
If a location is *not* specified, but the linked *part* has a default location,
|
||||
we can pre-fill the location automatically.
|
||||
"""
|
||||
|
||||
user = request.user
|
||||
|
||||
# Copy the request data, to side-step "mutability" issues
|
||||
@ -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))
|
||||
|
||||
def download_queryset(self, queryset, export_format):
|
||||
"""
|
||||
Download this queryset as a file.
|
||||
"""Download this queryset as a file.
|
||||
|
||||
Uses the APIDownloadMixin mixin class
|
||||
"""
|
||||
dataset = StockItemResource().export(queryset=queryset)
|
||||
@ -659,13 +616,11 @@ class StockList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
return DownloadFile(filedata, filename)
|
||||
|
||||
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.
|
||||
|
||||
So, we fetch and serialize the required StockLocation objects only as required.
|
||||
"""
|
||||
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
|
||||
params = request.query_params
|
||||
@ -775,9 +730,7 @@ class StockList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
return queryset
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
"""
|
||||
Custom filtering for the StockItem queryset
|
||||
"""
|
||||
"""Custom filtering for the StockItem queryset"""
|
||||
|
||||
params = self.request.query_params
|
||||
|
||||
@ -1090,9 +1043,7 @@ class StockList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
|
||||
|
||||
class StockAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
||||
"""
|
||||
API endpoint for listing (and creating) a StockItemAttachment (file upload)
|
||||
"""
|
||||
"""API endpoint for listing (and creating) a StockItemAttachment (file upload)"""
|
||||
|
||||
queryset = StockItemAttachment.objects.all()
|
||||
serializer_class = StockSerializers.StockItemAttachmentSerializer
|
||||
@ -1109,27 +1060,21 @@ class StockAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
||||
|
||||
|
||||
class StockAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin):
|
||||
"""
|
||||
Detail endpoint for StockItemAttachment
|
||||
"""
|
||||
"""Detail endpoint for StockItemAttachment"""
|
||||
|
||||
queryset = StockItemAttachment.objects.all()
|
||||
serializer_class = StockSerializers.StockItemAttachmentSerializer
|
||||
|
||||
|
||||
class StockItemTestResultDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
Detail endpoint for StockItemTestResult
|
||||
"""
|
||||
"""Detail endpoint for StockItemTestResult"""
|
||||
|
||||
queryset = StockItemTestResult.objects.all()
|
||||
serializer_class = StockSerializers.StockItemTestResultSerializer
|
||||
|
||||
|
||||
class StockItemTestResultList(generics.ListCreateAPIView):
|
||||
"""
|
||||
API endpoint for listing (and creating) a StockItemTestResult object.
|
||||
"""
|
||||
"""API endpoint for listing (and creating) a StockItemTestResult object."""
|
||||
|
||||
queryset = StockItemTestResult.objects.all()
|
||||
serializer_class = StockSerializers.StockItemTestResultSerializer
|
||||
@ -1205,8 +1150,7 @@ class StockItemTestResultList(generics.ListCreateAPIView):
|
||||
return self.serializer_class(*args, **kwargs)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""
|
||||
Create a new test result object.
|
||||
"""Create a new test result object.
|
||||
|
||||
Also, check if an attachment was uploaded alongside the test result,
|
||||
and save it to the database if it were.
|
||||
@ -1219,16 +1163,14 @@ class StockItemTestResultList(generics.ListCreateAPIView):
|
||||
|
||||
|
||||
class StockTrackingDetail(generics.RetrieveAPIView):
|
||||
"""
|
||||
Detail API endpoint for StockItemTracking model
|
||||
"""
|
||||
"""Detail API endpoint for StockItemTracking model"""
|
||||
|
||||
queryset = StockItemTracking.objects.all()
|
||||
serializer_class = StockSerializers.StockTrackingSerializer
|
||||
|
||||
|
||||
class StockTrackingList(generics.ListAPIView):
|
||||
""" API endpoint for list view of StockItemTracking objects.
|
||||
"""API endpoint for list view of StockItemTracking objects.
|
||||
|
||||
StockItemTracking objects are read-only
|
||||
(they are created by internal model functionality)
|
||||
@ -1320,7 +1262,7 @@ class StockTrackingList(generics.ListAPIView):
|
||||
return Response(data)
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
""" Create a new StockItemTracking object
|
||||
"""Create a new StockItemTracking object
|
||||
|
||||
Here we override the default 'create' implementation,
|
||||
to save the user information associated with the request object.
|
||||
@ -1374,7 +1316,7 @@ class LocationMetadata(generics.RetrieveUpdateAPIView):
|
||||
|
||||
|
||||
class LocationDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
""" API endpoint for detail view of StockLocation object
|
||||
"""API endpoint for detail view of StockLocation object
|
||||
|
||||
- GET: Return a single StockLocation object
|
||||
- PATCH: Update a StockLocation object
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
Django Forms for interacting with Stock app
|
||||
"""
|
||||
"""Django Forms for interacting with Stock app"""
|
||||
|
||||
from InvenTree.forms import HelperForm
|
||||
|
||||
@ -8,8 +6,7 @@ from .models import StockItem, StockItemTracking
|
||||
|
||||
|
||||
class ReturnStockItemForm(HelperForm):
|
||||
"""
|
||||
Form for manually returning a StockItem into stock
|
||||
"""Form for manually returning a StockItem into stock
|
||||
|
||||
TODO: This could be a simple API driven form!
|
||||
"""
|
||||
@ -22,8 +19,7 @@ class ReturnStockItemForm(HelperForm):
|
||||
|
||||
|
||||
class ConvertStockItemForm(HelperForm):
|
||||
"""
|
||||
Form for converting a StockItem to a variant of its current part.
|
||||
"""Form for converting a StockItem to a variant of its current part.
|
||||
|
||||
TODO: Migrate this form to the modern API forms interface
|
||||
"""
|
||||
@ -36,8 +32,7 @@ class ConvertStockItemForm(HelperForm):
|
||||
|
||||
|
||||
class TrackingEntryForm(HelperForm):
|
||||
"""
|
||||
Form for creating / editing a StockItemTracking object.
|
||||
"""Form for creating / editing a StockItemTracking object.
|
||||
|
||||
Note: 2021-05-11 - This form is not currently used - should delete?
|
||||
"""
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
Stock database model definitions
|
||||
"""
|
||||
"""Stock database model definitions"""
|
||||
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
@ -40,17 +38,16 @@ from users.models import Owner
|
||||
|
||||
|
||||
class StockLocation(MetadataMixin, InvenTreeTree):
|
||||
""" Organization tree for StockItem objects
|
||||
"""Organization tree for StockItem objects.
|
||||
|
||||
A "StockLocation" can be considered a warehouse, or storage location
|
||||
Stock locations can be heirarchical as required
|
||||
"""
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
with transaction.atomic():
|
||||
|
||||
parent = self.parent
|
||||
@ -84,12 +81,10 @@ class StockLocation(MetadataMixin, InvenTreeTree):
|
||||
related_name='stock_locations')
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
for loc in self.get_ancestors(include_self=True, ascending=True):
|
||||
if loc.owner is not None:
|
||||
return loc.owner
|
||||
@ -97,10 +92,7 @@ class StockLocation(MetadataMixin, InvenTreeTree):
|
||||
return None
|
||||
|
||||
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
|
||||
if user.is_superuser:
|
||||
return True
|
||||
@ -124,8 +116,7 @@ class StockLocation(MetadataMixin, InvenTreeTree):
|
||||
return reverse('stock-location-detail', kwargs={'pk': self.id})
|
||||
|
||||
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(
|
||||
'stocklocation',
|
||||
self.pk,
|
||||
@ -138,18 +129,15 @@ class StockLocation(MetadataMixin, InvenTreeTree):
|
||||
|
||||
@property
|
||||
def barcode(self):
|
||||
"""
|
||||
Brief payload data (e.g. for labels)
|
||||
"""
|
||||
"""Brief payload data (e.g. for labels)"""
|
||||
return self.format_barcode(brief=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:
|
||||
cascade: If True, also look under sublocations (default = True)
|
||||
"""
|
||||
|
||||
if cascade:
|
||||
query = StockItem.objects.filter(location__in=self.getUniqueChildren(include_self=True))
|
||||
else:
|
||||
@ -158,13 +146,11 @@ class StockLocation(MetadataMixin, InvenTreeTree):
|
||||
return query
|
||||
|
||||
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()
|
||||
|
||||
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:
|
||||
cascade: If True, also search an sublocations (default = True)
|
||||
@ -173,15 +159,14 @@ class StockLocation(MetadataMixin, InvenTreeTree):
|
||||
|
||||
@property
|
||||
def item_count(self):
|
||||
""" Simply returns the number of stock items in this location.
|
||||
Required for tree view serializer.
|
||||
"""
|
||||
"""Simply returns the number of stock items in this location.
|
||||
|
||||
Required for tree view serializer."""
|
||||
return self.stock_item_count()
|
||||
|
||||
|
||||
class StockItemManager(TreeManager):
|
||||
"""
|
||||
Custom database manager for the StockItem class.
|
||||
"""Custom database manager for the StockItem class.
|
||||
|
||||
StockItem querysets will automatically prefetch related fields.
|
||||
"""
|
||||
@ -205,13 +190,11 @@ class StockItemManager(TreeManager):
|
||||
|
||||
|
||||
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),
|
||||
which can be passed through a simple template.
|
||||
"""
|
||||
|
||||
batch_template = common.models.InvenTreeSetting.get_setting('STOCK_BATCH_CODE_TEMPLATE', '')
|
||||
|
||||
now = datetime.now()
|
||||
@ -231,8 +214,7 @@ def generate_batch_code():
|
||||
|
||||
|
||||
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:
|
||||
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())
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
serial = getattr(self, 'serial', '')
|
||||
|
||||
# Default value if we cannot convert to an integer
|
||||
@ -309,8 +290,7 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
self.serial_int = serial_int
|
||||
|
||||
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
|
||||
|
||||
@ -322,9 +302,7 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
|
||||
Returns:
|
||||
A StockItem object matching the requirements, or None
|
||||
|
||||
"""
|
||||
|
||||
if not self.serialized:
|
||||
return None
|
||||
|
||||
@ -358,13 +336,11 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
return None
|
||||
|
||||
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
|
||||
- Adds a transaction note when the item is first created.
|
||||
"""
|
||||
|
||||
self.validate_unique()
|
||||
self.clean()
|
||||
|
||||
@ -433,16 +409,15 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
|
||||
@property
|
||||
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
|
||||
|
||||
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.
|
||||
cannot exist for the same part (or part tree).
|
||||
"""
|
||||
|
||||
super(StockItem, self).validate_unique(exclude)
|
||||
|
||||
# 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")})
|
||||
|
||||
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:
|
||||
|
||||
@ -467,7 +442,6 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
- The 'part' does not belong to itself
|
||||
- Quantity must be 1 if the StockItem has a serial number
|
||||
"""
|
||||
|
||||
super().clean()
|
||||
|
||||
# Strip serial number field
|
||||
@ -563,7 +537,7 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
return self.part.full_name
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
"""
|
||||
|
||||
return InvenTree.helpers.MakeBarcode(
|
||||
"stockitem",
|
||||
self.id,
|
||||
@ -586,9 +559,7 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
|
||||
@property
|
||||
def barcode(self):
|
||||
"""
|
||||
Brief payload data (e.g. for labels)
|
||||
"""
|
||||
"""Brief payload data (e.g. for labels)"""
|
||||
return self.format_barcode(brief=True)
|
||||
|
||||
uid = models.CharField(blank=True, max_length=128, help_text=("Unique identifier field"))
|
||||
@ -753,11 +724,7 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
|
||||
@transaction.atomic
|
||||
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:
|
||||
# Ignore null values
|
||||
return
|
||||
@ -779,14 +746,12 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
)
|
||||
|
||||
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 is "in stock", check the StockLocation
|
||||
- Otherwise, return None
|
||||
"""
|
||||
|
||||
if self.owner is not None:
|
||||
return self.owner
|
||||
|
||||
@ -799,10 +764,7 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
return None
|
||||
|
||||
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
|
||||
if user.is_superuser:
|
||||
return True
|
||||
@ -821,8 +783,7 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
return user in owner.get_related_owners(include_group=True)
|
||||
|
||||
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:
|
||||
|
||||
@ -830,7 +791,6 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
- Expiry date will "expire" within the configured stale date
|
||||
- The StockItem is otherwise "in stock"
|
||||
"""
|
||||
|
||||
if self.expiry_date is None:
|
||||
return False
|
||||
|
||||
@ -849,8 +809,7 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
return self.expiry_date < expiry_date
|
||||
|
||||
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:
|
||||
|
||||
@ -858,7 +817,6 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
- Expiry date is "in the past"
|
||||
- The StockItem is otherwise "in stock"
|
||||
"""
|
||||
|
||||
if self.expiry_date is None:
|
||||
return False
|
||||
|
||||
@ -870,13 +828,11 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
return self.expiry_date < today
|
||||
|
||||
def clearAllocations(self):
|
||||
"""
|
||||
Clear all order allocations for this StockItem:
|
||||
"""Clear all order allocations for this StockItem:
|
||||
|
||||
- SalesOrder allocations
|
||||
- Build allocations
|
||||
"""
|
||||
|
||||
# Delete outstanding SalesOrder allocations
|
||||
self.sales_order_allocations.all().delete()
|
||||
|
||||
@ -884,8 +840,7 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
self.allocations.all().delete()
|
||||
|
||||
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:
|
||||
- Completion of a SalesOrder
|
||||
@ -898,7 +853,6 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
user: User that performed the action
|
||||
notes: Notes field
|
||||
"""
|
||||
|
||||
if quantity is None:
|
||||
quantity = self.quantity
|
||||
|
||||
@ -936,10 +890,7 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
return item
|
||||
|
||||
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', '')
|
||||
|
||||
tracking_info = {}
|
||||
@ -972,10 +923,7 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
infinite = models.BooleanField(default=False)
|
||||
|
||||
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 - In future, once the "build" is working better, check this too
|
||||
|
||||
@ -988,10 +936,7 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
return False
|
||||
|
||||
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)))
|
||||
|
||||
total = query['q']
|
||||
@ -1002,10 +947,7 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
return total
|
||||
|
||||
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)))
|
||||
|
||||
total = query['q']
|
||||
@ -1016,31 +958,24 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
return total
|
||||
|
||||
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()
|
||||
so = self.sales_order_allocation_count()
|
||||
|
||||
return bo + so
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
- Is installed inside another StockItem
|
||||
- It has been assigned to a SalesOrder
|
||||
- It has been assigned to a BuildOrder
|
||||
"""
|
||||
|
||||
if self.installed_item_count() > 0:
|
||||
return False
|
||||
|
||||
@ -1050,15 +985,13 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
return True
|
||||
|
||||
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:
|
||||
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!
|
||||
"""
|
||||
|
||||
installed = set()
|
||||
|
||||
items = StockItem.objects.filter(belongs_to=self)
|
||||
@ -1085,16 +1018,12 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
return installed
|
||||
|
||||
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()
|
||||
|
||||
@transaction.atomic
|
||||
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
|
||||
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
|
||||
notes: Any notes associated with the operation
|
||||
"""
|
||||
|
||||
# Cannot be already installed in another stock item!
|
||||
if self.belongs_to is not None:
|
||||
return False
|
||||
@ -1139,15 +1067,13 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
|
||||
@transaction.atomic
|
||||
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:
|
||||
location: The stock location where the item will be moved
|
||||
user: The user performing the operation
|
||||
notes: Any notes associated with the operation
|
||||
"""
|
||||
|
||||
# If the stock item is not installed in anything, ignore
|
||||
if self.belongs_to is None:
|
||||
return False
|
||||
@ -1184,24 +1110,22 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
|
||||
@property
|
||||
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)
|
||||
|
||||
@property
|
||||
def child_count(self):
|
||||
""" Return the number of 'child' items associated with this StockItem.
|
||||
A child item is one which has been split from this one.
|
||||
"""
|
||||
"""Return the number of 'child' items associated with this StockItem.
|
||||
|
||||
A child item is one which has been split from this one."""
|
||||
return self.children.count()
|
||||
|
||||
@property
|
||||
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
|
||||
"""
|
||||
|
||||
query = StockItem.objects.filter(pk=self.pk)
|
||||
|
||||
query = query.filter(StockItem.IN_STOCK_FILTER)
|
||||
@ -1210,14 +1134,12 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
|
||||
@property
|
||||
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:
|
||||
- Has been delivered to a customer
|
||||
- Has been installed inside another StockItem
|
||||
"""
|
||||
|
||||
if self.customer is not None:
|
||||
return False
|
||||
|
||||
@ -1238,8 +1160,7 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
return self.tracking_info_count > 0
|
||||
|
||||
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:
|
||||
entry_type - Integer code describing the "type" of historical action (see StockHistoryCode)
|
||||
@ -1276,7 +1197,7 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
|
||||
@transaction.atomic
|
||||
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
|
||||
- Number of serial numbers must match the quantity
|
||||
@ -1289,7 +1210,6 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
notes: Optional notes for tracking
|
||||
location: If specified, serialized items will be placed in the given location
|
||||
"""
|
||||
|
||||
# Cannot serialize stock that is already serialized!
|
||||
if self.serialized:
|
||||
return
|
||||
@ -1360,8 +1280,7 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
|
||||
@transaction.atomic
|
||||
def copyHistoryFrom(self, other):
|
||||
""" Copy stock history from another StockItem """
|
||||
|
||||
"""Copy stock history from another StockItem"""
|
||||
for item in other.tracking_info.all():
|
||||
|
||||
item.item = self
|
||||
@ -1370,8 +1289,7 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
|
||||
@transaction.atomic
|
||||
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):
|
||||
|
||||
# Create a copy of the test result by nulling-out the pk
|
||||
@ -1380,10 +1298,7 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
result.save()
|
||||
|
||||
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_status = kwargs.get('allow_mismatched_status', False)
|
||||
@ -1437,8 +1352,7 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
|
||||
@transaction.atomic
|
||||
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:
|
||||
|
||||
@ -1446,7 +1360,6 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
- Tracking history for the *other* item is deleted
|
||||
- Any allocations (build order, sales order) are moved to this StockItem
|
||||
"""
|
||||
|
||||
if len(other_items) == 0:
|
||||
return
|
||||
|
||||
@ -1499,7 +1412,8 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
|
||||
@transaction.atomic
|
||||
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,
|
||||
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 new item will have a different StockItem ID, while this will remain the same.
|
||||
"""
|
||||
|
||||
notes = kwargs.get('notes', '')
|
||||
code = kwargs.get('code', StockHistoryCode.SPLIT_FROM_PARENT)
|
||||
|
||||
@ -1576,7 +1489,7 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
|
||||
@transaction.atomic
|
||||
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,
|
||||
a new StockItem is created, with the defined quantity,
|
||||
@ -1590,7 +1503,6 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
kwargs:
|
||||
quantity: If provided, override the quantity (default = total stock quantity)
|
||||
"""
|
||||
|
||||
try:
|
||||
quantity = Decimal(kwargs.get('quantity', self.quantity))
|
||||
except InvalidOperation:
|
||||
@ -1636,7 +1548,7 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
|
||||
@transaction.atomic
|
||||
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.
|
||||
|
||||
@ -1644,7 +1556,6 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
- True if the quantity was saved
|
||||
- False if the StockItem was deleted
|
||||
"""
|
||||
|
||||
# Do not adjust quantity of a serialized part
|
||||
if self.serialized:
|
||||
return
|
||||
@ -1669,11 +1580,10 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
|
||||
@transaction.atomic
|
||||
def stocktake(self, count, user, notes=''):
|
||||
""" Perform item stocktake.
|
||||
"""Perform item stocktake.
|
||||
When the quantity of an item is counted,
|
||||
record the date of stocktake
|
||||
"""
|
||||
|
||||
try:
|
||||
count = Decimal(count)
|
||||
except InvalidOperation:
|
||||
@ -1700,11 +1610,10 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
|
||||
@transaction.atomic
|
||||
def add_stock(self, quantity, user, notes=''):
|
||||
""" Add items to stock
|
||||
"""Add items to stock
|
||||
This function can be called by initiating a ProjectRun,
|
||||
or by manually adding the items to the stock location
|
||||
"""
|
||||
|
||||
# Cannot add items to a serialized part
|
||||
if self.serialized:
|
||||
return False
|
||||
@ -1734,10 +1643,7 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
|
||||
@transaction.atomic
|
||||
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
|
||||
if self.serialized:
|
||||
return False
|
||||
@ -1787,13 +1693,7 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
|
||||
@transaction.atomic
|
||||
def clear_test_results(self, **kwargs):
|
||||
"""
|
||||
Remove all test results
|
||||
|
||||
kwargs:
|
||||
TODO
|
||||
"""
|
||||
|
||||
"""Remove all test results"""
|
||||
# All test results
|
||||
results = self.test_results.all()
|
||||
|
||||
@ -1802,15 +1702,13 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
results.delete()
|
||||
|
||||
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:
|
||||
- Test name
|
||||
- Test result
|
||||
- User
|
||||
"""
|
||||
|
||||
results = self.test_results
|
||||
|
||||
if test:
|
||||
@ -1828,15 +1726,13 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
return results
|
||||
|
||||
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,
|
||||
the *most recent* test is used.
|
||||
|
||||
This map is useful for rendering to a template (e.g. a test report),
|
||||
as all named tests are accessible.
|
||||
"""
|
||||
|
||||
# Do we wish to include test results from installed items?
|
||||
include_installed = kwargs.pop('include_installed', False)
|
||||
|
||||
@ -1867,15 +1763,11 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
return result_map
|
||||
|
||||
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()
|
||||
|
||||
def requiredTestStatus(self):
|
||||
"""
|
||||
Return the status of the tests required for this StockItem.
|
||||
"""Return the status of the tests required for this StockItem.
|
||||
|
||||
return:
|
||||
A dict containing the following items:
|
||||
@ -1883,7 +1775,6 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
- passed: Number of tests that have passed
|
||||
- failed: Number of tests that have failed
|
||||
"""
|
||||
|
||||
# All the tests required by the part object
|
||||
required = self.part.getRequiredTests()
|
||||
|
||||
@ -1912,31 +1803,21 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
|
||||
@property
|
||||
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()
|
||||
|
||||
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
|
||||
|
||||
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()
|
||||
|
||||
return status['passed'] >= status['total']
|
||||
|
||||
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 = []
|
||||
|
||||
item_query = StockItem.objects.filter(pk=self.pk)
|
||||
@ -1955,17 +1836,11 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
|
||||
@property
|
||||
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
|
||||
|
||||
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 = []
|
||||
|
||||
item_query = StockItem.objects.filter(pk=self.pk)
|
||||
@ -1984,22 +1859,17 @@ class StockItem(MetadataMixin, MPTTModel):
|
||||
|
||||
@property
|
||||
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
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=StockItem, dispatch_uid='stock_item_pre_delete_log')
|
||||
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,
|
||||
to point to the new parent item.
|
||||
"""
|
||||
|
||||
# Update each StockItem parent field
|
||||
for child in instance.children.all():
|
||||
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')
|
||||
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
|
||||
|
||||
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')
|
||||
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
|
||||
|
||||
if not InvenTree.ready.isImportingData():
|
||||
@ -2050,8 +1916,7 @@ class StockItemAttachment(InvenTreeAttachment):
|
||||
|
||||
|
||||
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
|
||||
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):
|
||||
"""
|
||||
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
|
||||
with automated testing setups.
|
||||
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
JSON serializers for Stock app
|
||||
"""
|
||||
"""JSON serializers for Stock app"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
@ -29,9 +27,7 @@ from .models import (StockItem, StockItemAttachment, StockItemTestResult,
|
||||
|
||||
|
||||
class LocationBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
"""
|
||||
Provides a brief serializer for a StockLocation object
|
||||
"""
|
||||
"""Provides a brief serializer for a StockLocation object"""
|
||||
|
||||
class Meta:
|
||||
model = StockLocation
|
||||
@ -43,7 +39,7 @@ class LocationBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
|
||||
|
||||
class StockItemSerializerBrief(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
""" Brief serializers for a StockItem """
|
||||
"""Brief serializers for a StockItem"""
|
||||
|
||||
location_name = serializers.CharField(source='location', read_only=True)
|
||||
part_name = serializers.CharField(source='part.full_name', read_only=True)
|
||||
@ -71,7 +67,7 @@ class StockItemSerializerBrief(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 item location
|
||||
@ -88,11 +84,7 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
|
||||
@staticmethod
|
||||
def annotate_queryset(queryset):
|
||||
"""
|
||||
Add some extra annotations to the queryset,
|
||||
performing database queries as efficiently as possible.
|
||||
"""
|
||||
|
||||
"""Add some extra annotations to the queryset, performing database queries as efficiently as possible."""
|
||||
# Annotate the queryset with the total allocated to sales orders
|
||||
queryset = queryset.annotate(
|
||||
allocated=Coalesce(
|
||||
@ -257,8 +249,7 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
|
||||
|
||||
class SerializeStockItemSerializer(serializers.Serializer):
|
||||
"""
|
||||
A DRF serializer for "serializing" a StockItem.
|
||||
"""A DRF serializer for "serializing" a StockItem.
|
||||
|
||||
(Sorry for the confusing naming...)
|
||||
|
||||
@ -284,9 +275,7 @@ class SerializeStockItemSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate_quantity(self, quantity):
|
||||
"""
|
||||
Validate that the quantity value is correct
|
||||
"""
|
||||
"""Validate that the quantity value is correct"""
|
||||
|
||||
item = self.context['item']
|
||||
|
||||
@ -323,9 +312,7 @@ class SerializeStockItemSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate(self, data):
|
||||
"""
|
||||
Check that the supplied serial numbers are valid
|
||||
"""
|
||||
"""Check that the supplied serial numbers are valid"""
|
||||
|
||||
data = super().validate(data)
|
||||
|
||||
@ -381,9 +368,7 @@ class SerializeStockItemSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class InstallStockItemSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for installing a stock item into a given part
|
||||
"""
|
||||
"""Serializer for installing a stock item into a given part"""
|
||||
|
||||
stock_item = serializers.PrimaryKeyRelatedField(
|
||||
queryset=StockItem.objects.all(),
|
||||
@ -401,9 +386,7 @@ class InstallStockItemSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate_stock_item(self, stock_item):
|
||||
"""
|
||||
Validate the selected stock item
|
||||
"""
|
||||
"""Validate the selected stock item"""
|
||||
|
||||
if not stock_item.in_stock:
|
||||
# StockItem must be in stock to be "installed"
|
||||
@ -419,7 +402,7 @@ class InstallStockItemSerializer(serializers.Serializer):
|
||||
return stock_item
|
||||
|
||||
def save(self):
|
||||
""" Install the selected stock item into this one """
|
||||
"""Install the selected stock item into this one"""
|
||||
|
||||
data = self.validated_data
|
||||
|
||||
@ -438,9 +421,7 @@ class InstallStockItemSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class UninstallStockItemSerializer(serializers.Serializer):
|
||||
"""
|
||||
API serializers for uninstalling an installed item from a stock item
|
||||
"""
|
||||
"""API serializers for uninstalling an installed item from a stock item"""
|
||||
|
||||
class Meta:
|
||||
fields = [
|
||||
@ -480,9 +461,7 @@ class UninstallStockItemSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class LocationTreeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
"""
|
||||
Serializer for a simple tree view
|
||||
"""
|
||||
"""Serializer for a simple tree view"""
|
||||
|
||||
class Meta:
|
||||
model = StockLocation
|
||||
@ -494,8 +473,7 @@ class LocationTreeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
|
||||
|
||||
class LocationSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
""" Detailed information about a stock location
|
||||
"""
|
||||
"""Detailed information about a stock location"""
|
||||
|
||||
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
||||
|
||||
@ -519,7 +497,7 @@ class LocationSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
|
||||
|
||||
class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSerializer):
|
||||
""" Serializer for StockItemAttachment model """
|
||||
"""Serializer for StockItemAttachment model"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
user_detail = kwargs.pop('user_detail', False)
|
||||
@ -556,7 +534,7 @@ class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSer
|
||||
|
||||
|
||||
class StockItemTestResultSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
""" Serializer for the StockItemTestResult model """
|
||||
"""Serializer for the StockItemTestResult model"""
|
||||
|
||||
user_detail = InvenTree.serializers.UserSerializerBrief(source='user', read_only=True)
|
||||
|
||||
@ -597,7 +575,7 @@ class StockItemTestResultSerializer(InvenTree.serializers.InvenTreeModelSerializ
|
||||
|
||||
|
||||
class StockTrackingSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
""" Serializer for StockItemTracking model """
|
||||
"""Serializer for StockItemTracking model"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@ -644,8 +622,7 @@ class StockTrackingSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
|
||||
|
||||
class StockAssignmentItemSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for a single StockItem with in StockAssignment request.
|
||||
"""Serializer for a single StockItem with in StockAssignment request.
|
||||
|
||||
Here, the particular StockItem is being assigned (manually) to a customer
|
||||
|
||||
@ -688,8 +665,7 @@ class StockAssignmentItemSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class StockAssignmentSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for assigning one (or more) stock items to a customer.
|
||||
"""Serializer for assigning one (or more) stock items to a customer.
|
||||
|
||||
This is a manual assignment process, separate for (for example) a Sales Order
|
||||
"""
|
||||
@ -765,8 +741,7 @@ class StockAssignmentSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class StockMergeItemSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for a single StockItem within the StockMergeSerializer class.
|
||||
"""Serializer for a single StockItem within the StockMergeSerializer class.
|
||||
|
||||
Here, the individual StockItem is being checked for merge compatibility.
|
||||
"""
|
||||
@ -793,9 +768,7 @@ class StockMergeItemSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class StockMergeSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for merging two (or more) stock items together
|
||||
"""
|
||||
"""Serializer for merging two (or more) stock items together"""
|
||||
|
||||
class Meta:
|
||||
fields = [
|
||||
@ -879,8 +852,8 @@ class StockMergeSerializer(serializers.Serializer):
|
||||
return data
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Actually perform the stock merging action.
|
||||
"""Actually perform the stock merging action.
|
||||
|
||||
At this point we are confident that the merge can take place
|
||||
"""
|
||||
|
||||
@ -908,8 +881,7 @@ class StockMergeSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class StockAdjustmentItemSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for a single StockItem within a stock adjument request.
|
||||
"""Serializer for a single StockItem within a stock adjument request.
|
||||
|
||||
Fields:
|
||||
- item: StockItem object
|
||||
@ -940,9 +912,7 @@ class StockAdjustmentItemSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class StockAdjustmentSerializer(serializers.Serializer):
|
||||
"""
|
||||
Base class for managing stock adjustment actions via the API
|
||||
"""
|
||||
"""Base class for managing stock adjustment actions via the API"""
|
||||
|
||||
class Meta:
|
||||
fields = [
|
||||
@ -972,9 +942,7 @@ class StockAdjustmentSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class StockCountSerializer(StockAdjustmentSerializer):
|
||||
"""
|
||||
Serializer for counting stock items
|
||||
"""
|
||||
"""Serializer for counting stock items"""
|
||||
|
||||
def save(self):
|
||||
|
||||
@ -998,9 +966,7 @@ class StockCountSerializer(StockAdjustmentSerializer):
|
||||
|
||||
|
||||
class StockAddSerializer(StockAdjustmentSerializer):
|
||||
"""
|
||||
Serializer for adding stock to stock item(s)
|
||||
"""
|
||||
"""Serializer for adding stock to stock item(s)"""
|
||||
|
||||
def save(self):
|
||||
|
||||
@ -1023,9 +989,7 @@ class StockAddSerializer(StockAdjustmentSerializer):
|
||||
|
||||
|
||||
class StockRemoveSerializer(StockAdjustmentSerializer):
|
||||
"""
|
||||
Serializer for removing stock from stock item(s)
|
||||
"""
|
||||
"""Serializer for removing stock from stock item(s)"""
|
||||
|
||||
def save(self):
|
||||
|
||||
@ -1048,9 +1012,7 @@ class StockRemoveSerializer(StockAdjustmentSerializer):
|
||||
|
||||
|
||||
class StockTransferSerializer(StockAdjustmentSerializer):
|
||||
"""
|
||||
Serializer for transferring (moving) stock item(s)
|
||||
"""
|
||||
"""Serializer for transferring (moving) stock item(s)"""
|
||||
|
||||
location = serializers.PrimaryKeyRelatedField(
|
||||
queryset=StockLocation.objects.all(),
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
Unit testing for the Stock API
|
||||
"""
|
||||
"""Unit testing for the Stock API"""
|
||||
|
||||
import io
|
||||
import os
|
||||
@ -47,9 +45,7 @@ class StockAPITestCase(InvenTreeAPITestCase):
|
||||
|
||||
|
||||
class StockLocationTest(StockAPITestCase):
|
||||
"""
|
||||
Series of API tests for the StockLocation API
|
||||
"""
|
||||
"""Series of API tests for the StockLocation API"""
|
||||
list_url = reverse('api-location-list')
|
||||
|
||||
def setUp(self):
|
||||
@ -76,16 +72,12 @@ class StockLocationTest(StockAPITestCase):
|
||||
|
||||
|
||||
class StockItemListTest(StockAPITestCase):
|
||||
"""
|
||||
Tests for the StockItem API LIST endpoint
|
||||
"""
|
||||
"""Tests for the StockItem API LIST endpoint"""
|
||||
|
||||
list_url = reverse('api-stock-list')
|
||||
|
||||
def get_stock(self, **kwargs):
|
||||
"""
|
||||
Filter stock and return JSON object
|
||||
"""
|
||||
"""Filter stock and return JSON object"""
|
||||
|
||||
response = self.client.get(self.list_url, format='json', data=kwargs)
|
||||
|
||||
@ -95,18 +87,14 @@ class StockItemListTest(StockAPITestCase):
|
||||
return response.data
|
||||
|
||||
def test_get_stock_list(self):
|
||||
"""
|
||||
List *all* StockItem objects.
|
||||
"""
|
||||
"""List *all* StockItem objects."""
|
||||
|
||||
response = self.get_stock()
|
||||
|
||||
self.assertEqual(len(response), 29)
|
||||
|
||||
def test_filter_by_part(self):
|
||||
"""
|
||||
Filter StockItem by Part reference
|
||||
"""
|
||||
"""Filter StockItem by Part reference"""
|
||||
|
||||
response = self.get_stock(part=25)
|
||||
|
||||
@ -117,17 +105,13 @@ class StockItemListTest(StockAPITestCase):
|
||||
self.assertEqual(len(response), 12)
|
||||
|
||||
def test_filter_by_IPN(self):
|
||||
"""
|
||||
Filter StockItem by IPN reference
|
||||
"""
|
||||
"""Filter StockItem by IPN reference"""
|
||||
|
||||
response = self.get_stock(IPN="R.CH")
|
||||
self.assertEqual(len(response), 3)
|
||||
|
||||
def test_filter_by_location(self):
|
||||
"""
|
||||
Filter StockItem by StockLocation reference
|
||||
"""
|
||||
"""Filter StockItem by StockLocation reference"""
|
||||
|
||||
response = self.get_stock(location=5)
|
||||
self.assertEqual(len(response), 1)
|
||||
@ -142,9 +126,7 @@ class StockItemListTest(StockAPITestCase):
|
||||
self.assertEqual(len(response), 18)
|
||||
|
||||
def test_filter_by_depleted(self):
|
||||
"""
|
||||
Filter StockItem by depleted status
|
||||
"""
|
||||
"""Filter StockItem by depleted status"""
|
||||
|
||||
response = self.get_stock(depleted=1)
|
||||
self.assertEqual(len(response), 1)
|
||||
@ -153,9 +135,7 @@ class StockItemListTest(StockAPITestCase):
|
||||
self.assertEqual(len(response), 28)
|
||||
|
||||
def test_filter_by_in_stock(self):
|
||||
"""
|
||||
Filter StockItem by 'in stock' status
|
||||
"""
|
||||
"""Filter StockItem by 'in stock' status"""
|
||||
|
||||
response = self.get_stock(in_stock=1)
|
||||
self.assertEqual(len(response), 26)
|
||||
@ -164,9 +144,7 @@ class StockItemListTest(StockAPITestCase):
|
||||
self.assertEqual(len(response), 3)
|
||||
|
||||
def test_filter_by_status(self):
|
||||
"""
|
||||
Filter StockItem by 'status' field
|
||||
"""
|
||||
"""Filter StockItem by 'status' field"""
|
||||
|
||||
codes = {
|
||||
StockStatus.OK: 27,
|
||||
@ -183,17 +161,13 @@ class StockItemListTest(StockAPITestCase):
|
||||
self.assertEqual(len(response), num)
|
||||
|
||||
def test_filter_by_batch(self):
|
||||
"""
|
||||
Filter StockItem by batch code
|
||||
"""
|
||||
"""Filter StockItem by batch code"""
|
||||
|
||||
response = self.get_stock(batch='B123')
|
||||
self.assertEqual(len(response), 1)
|
||||
|
||||
def test_filter_by_serialized(self):
|
||||
"""
|
||||
Filter StockItem by serialized status
|
||||
"""
|
||||
"""Filter StockItem by serialized status"""
|
||||
|
||||
response = self.get_stock(serialized=1)
|
||||
self.assertEqual(len(response), 12)
|
||||
@ -208,9 +182,7 @@ class StockItemListTest(StockAPITestCase):
|
||||
self.assertIsNone(item['serial'])
|
||||
|
||||
def test_filter_by_has_batch(self):
|
||||
"""
|
||||
Test the 'has_batch' filter, which tests if the stock item has been assigned a batch code
|
||||
"""
|
||||
"""Test the 'has_batch' filter, which tests if the stock item has been assigned a batch code"""
|
||||
|
||||
with_batch = self.get_stock(has_batch=1)
|
||||
without_batch = self.get_stock(has_batch=0)
|
||||
@ -227,8 +199,7 @@ class StockItemListTest(StockAPITestCase):
|
||||
self.assertTrue(item['batch'] in [None, ''])
|
||||
|
||||
def test_filter_by_tracked(self):
|
||||
"""
|
||||
Test the 'tracked' filter.
|
||||
"""Test the 'tracked' filter.
|
||||
This checks if the stock item has either a batch code *or* a serial number
|
||||
"""
|
||||
|
||||
@ -248,9 +219,7 @@ class StockItemListTest(StockAPITestCase):
|
||||
self.assertTrue(item['batch'] in blank and item['serial'] in blank)
|
||||
|
||||
def test_filter_by_expired(self):
|
||||
"""
|
||||
Filter StockItem by expiry status
|
||||
"""
|
||||
"""Filter StockItem by expiry status"""
|
||||
|
||||
# First, we can assume that the 'stock expiry' feature is disabled
|
||||
response = self.get_stock(expired=1)
|
||||
@ -289,9 +258,7 @@ class StockItemListTest(StockAPITestCase):
|
||||
self.assertEqual(len(response), 25)
|
||||
|
||||
def test_paginate(self):
|
||||
"""
|
||||
Test that we can paginate results correctly
|
||||
"""
|
||||
"""Test that we can paginate results correctly"""
|
||||
|
||||
for n in [1, 5, 10]:
|
||||
response = self.get_stock(limit=n)
|
||||
@ -321,9 +288,7 @@ class StockItemListTest(StockAPITestCase):
|
||||
return dataset
|
||||
|
||||
def test_export(self):
|
||||
"""
|
||||
Test exporting of Stock data via the API
|
||||
"""
|
||||
"""Test exporting of Stock data via the API"""
|
||||
|
||||
dataset = self.export_data({})
|
||||
|
||||
@ -361,9 +326,7 @@ class StockItemListTest(StockAPITestCase):
|
||||
|
||||
|
||||
class StockItemTest(StockAPITestCase):
|
||||
"""
|
||||
Series of API tests for the StockItem API
|
||||
"""
|
||||
"""Series of API tests for the StockItem API"""
|
||||
|
||||
list_url = reverse('api-stock-list')
|
||||
|
||||
@ -376,8 +339,7 @@ class StockItemTest(StockAPITestCase):
|
||||
StockLocation.objects.create(name='C', description='location c', parent=top)
|
||||
|
||||
def test_create_default_location(self):
|
||||
"""
|
||||
Test the default location functionality,
|
||||
"""Test the default location functionality,
|
||||
if a 'location' is not specified in the creation request.
|
||||
"""
|
||||
|
||||
@ -423,9 +385,7 @@ class StockItemTest(StockAPITestCase):
|
||||
self.assertEqual(response.data['location'], None)
|
||||
|
||||
def test_stock_item_create(self):
|
||||
"""
|
||||
Test creation of a StockItem via the API
|
||||
"""
|
||||
"""Test creation of a StockItem via the API"""
|
||||
|
||||
# POST with an empty part reference
|
||||
|
||||
@ -476,9 +436,7 @@ class StockItemTest(StockAPITestCase):
|
||||
)
|
||||
|
||||
def test_creation_with_serials(self):
|
||||
"""
|
||||
Test that serialized stock items can be created via the API,
|
||||
"""
|
||||
"""Test that serialized stock items can be created via the API."""
|
||||
|
||||
trackable_part = part.models.Part.objects.create(
|
||||
name='My part',
|
||||
@ -587,9 +545,7 @@ class StockItemTest(StockAPITestCase):
|
||||
self.assertEqual(response.data['expiry_date'], expiry.isoformat())
|
||||
|
||||
def test_purchase_price(self):
|
||||
"""
|
||||
Test that we can correctly read and adjust purchase price information via the API
|
||||
"""
|
||||
"""Test that we can correctly read and adjust purchase price information via the API"""
|
||||
|
||||
url = reverse('api-stock-detail', kwargs={'pk': 1})
|
||||
|
||||
@ -648,7 +604,7 @@ class StockItemTest(StockAPITestCase):
|
||||
self.assertEqual(data['purchase_price_currency'], 'NZD')
|
||||
|
||||
def test_install(self):
|
||||
""" Test that stock item can be installed into antoher item, via the API """
|
||||
"""Test that stock item can be installed into antoher item, via the API"""
|
||||
|
||||
# Select the "parent" stock item
|
||||
parent_part = part.models.Part.objects.get(pk=100)
|
||||
@ -731,15 +687,10 @@ class StockItemTest(StockAPITestCase):
|
||||
|
||||
|
||||
class StocktakeTest(StockAPITestCase):
|
||||
"""
|
||||
Series of tests for the Stocktake API
|
||||
"""
|
||||
"""Series of tests for the Stocktake API"""
|
||||
|
||||
def test_action(self):
|
||||
"""
|
||||
Test each stocktake action endpoint,
|
||||
for validation
|
||||
"""
|
||||
"""Test each stocktake action endpoint, for validation"""
|
||||
|
||||
for endpoint in ['api-stock-count', 'api-stock-add', 'api-stock-remove']:
|
||||
|
||||
@ -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)
|
||||
|
||||
def test_transfer(self):
|
||||
"""
|
||||
Test stock transfers
|
||||
"""
|
||||
"""Test stock transfers"""
|
||||
|
||||
data = {
|
||||
'items': [
|
||||
@ -825,9 +774,7 @@ class StocktakeTest(StockAPITestCase):
|
||||
|
||||
|
||||
class StockItemDeletionTest(StockAPITestCase):
|
||||
"""
|
||||
Tests for stock item deletion via the API
|
||||
"""
|
||||
"""Tests for stock item deletion via the API"""
|
||||
|
||||
def test_delete(self):
|
||||
|
||||
@ -974,8 +921,7 @@ class StockTestResultTest(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
|
||||
"""
|
||||
|
||||
@ -1083,9 +1029,7 @@ class StockAssignTest(StockAPITestCase):
|
||||
|
||||
|
||||
class StockMergeTest(StockAPITestCase):
|
||||
"""
|
||||
Unit tests for merging stock items via the API
|
||||
"""
|
||||
"""Unit tests for merging stock items via the API"""
|
||||
|
||||
URL = reverse('api-stock-merge')
|
||||
|
||||
@ -1117,9 +1061,7 @@ class StockMergeTest(StockAPITestCase):
|
||||
)
|
||||
|
||||
def test_missing_data(self):
|
||||
"""
|
||||
Test responses which are missing required data
|
||||
"""
|
||||
"""Test responses which are missing required data"""
|
||||
|
||||
# Post completely empty
|
||||
|
||||
@ -1145,9 +1087,7 @@ class StockMergeTest(StockAPITestCase):
|
||||
self.assertIn('At least two stock items', str(data))
|
||||
|
||||
def test_invalid_data(self):
|
||||
"""
|
||||
Test responses which have invalid data
|
||||
"""
|
||||
"""Test responses which have invalid data"""
|
||||
|
||||
# Serialized stock items should be rejected
|
||||
data = self.post(
|
||||
@ -1229,9 +1169,7 @@ class StockMergeTest(StockAPITestCase):
|
||||
self.assertIn('Stock items must refer to the same supplier part', str(data))
|
||||
|
||||
def test_valid_merge(self):
|
||||
"""
|
||||
Test valid merging of stock items
|
||||
"""
|
||||
"""Test valid merging of stock items"""
|
||||
|
||||
# Check initial conditions
|
||||
n = StockItem.objects.filter(part=self.part).count()
|
||||
|
@ -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
|
||||
|
||||
@ -22,7 +22,7 @@ class StockViewTestCase(InvenTreeTestCase):
|
||||
|
||||
|
||||
class StockListTest(StockViewTestCase):
|
||||
""" Tests for Stock list views """
|
||||
"""Tests for Stock list views"""
|
||||
|
||||
def test_stock_index(self):
|
||||
response = self.client.get(reverse('stock-index'))
|
||||
@ -30,10 +30,10 @@ class StockListTest(StockViewTestCase):
|
||||
|
||||
|
||||
class StockOwnershipTest(StockViewTestCase):
|
||||
""" Tests for stock ownership views """
|
||||
"""Tests for stock ownership views"""
|
||||
|
||||
def setUp(self):
|
||||
""" Add another user for ownership tests """
|
||||
"""Add another user for ownership tests"""
|
||||
|
||||
"""
|
||||
TODO: Refactor this following test to use the new API form
|
||||
|
@ -13,9 +13,7 @@ from .models import (StockItem, StockItemTestResult, StockItemTracking,
|
||||
|
||||
|
||||
class StockTest(InvenTreeTestCase):
|
||||
"""
|
||||
Tests to ensure that the stock location tree functions correcly
|
||||
"""
|
||||
"""Tests to ensure that the stock location tree functions correcly"""
|
||||
|
||||
fixtures = [
|
||||
'category',
|
||||
@ -44,10 +42,7 @@ class StockTest(InvenTreeTestCase):
|
||||
StockItem.objects.rebuild()
|
||||
|
||||
def test_expiry(self):
|
||||
"""
|
||||
Test expiry date functionality for StockItem model.
|
||||
"""
|
||||
|
||||
"""Test expiry date functionality for StockItem model."""
|
||||
today = datetime.datetime.now().date()
|
||||
|
||||
item = StockItem.objects.create(
|
||||
@ -78,10 +73,7 @@ class StockTest(InvenTreeTestCase):
|
||||
self.assertTrue(item.is_expired())
|
||||
|
||||
def test_is_building(self):
|
||||
"""
|
||||
Test that the is_building flag does not count towards stock.
|
||||
"""
|
||||
|
||||
"""Test that the is_building flag does not count towards stock."""
|
||||
part = Part.objects.get(pk=1)
|
||||
|
||||
# Record the total stock count
|
||||
@ -197,7 +189,6 @@ class StockTest(InvenTreeTestCase):
|
||||
|
||||
def test_move(self):
|
||||
""" Test stock movement functions """
|
||||
|
||||
# Move 4,000 screws to the bathroom
|
||||
it = StockItem.objects.get(pk=1)
|
||||
self.assertNotEqual(it.location, self.bathroom)
|
||||
@ -339,10 +330,7 @@ class StockTest(InvenTreeTestCase):
|
||||
w2 = StockItem.objects.get(pk=101)
|
||||
|
||||
def test_serials(self):
|
||||
"""
|
||||
Tests for stock serialization
|
||||
"""
|
||||
|
||||
"""Tests for stock serialization"""
|
||||
p = Part.objects.create(
|
||||
name='trackable part',
|
||||
description='trackable part',
|
||||
@ -373,10 +361,7 @@ class StockTest(InvenTreeTestCase):
|
||||
self.assertTrue(item.serialized)
|
||||
|
||||
def test_big_serials(self):
|
||||
"""
|
||||
Unit tests for "large" serial numbers which exceed integer encoding
|
||||
"""
|
||||
|
||||
"""Unit tests for "large" serial numbers which exceed integer encoding"""
|
||||
p = Part.objects.create(
|
||||
name='trackable part',
|
||||
description='trackable part',
|
||||
@ -451,11 +436,10 @@ class StockTest(InvenTreeTestCase):
|
||||
self.assertEqual(item_prev.serial_int, 99)
|
||||
|
||||
def test_serialize_stock_invalid(self):
|
||||
"""
|
||||
Test manual serialization of parts.
|
||||
"""Test manual serialization of parts.
|
||||
|
||||
Each of these tests should fail
|
||||
"""
|
||||
|
||||
# Test serialization of non-serializable part
|
||||
item = StockItem.objects.get(pk=1234)
|
||||
|
||||
@ -480,8 +464,7 @@ class StockTest(InvenTreeTestCase):
|
||||
item.serializeStock(3, "hello", self.user)
|
||||
|
||||
def test_serialize_stock_valid(self):
|
||||
""" Perform valid stock serializations """
|
||||
|
||||
"""Perform valid stock serializations"""
|
||||
# There are 10 of these in stock
|
||||
# Item will deplete when deleted
|
||||
item = StockItem.objects.get(pk=100)
|
||||
@ -517,8 +500,8 @@ class StockTest(InvenTreeTestCase):
|
||||
item.serializeStock(2, [99, 100], self.user)
|
||||
|
||||
def test_location_tree(self):
|
||||
"""
|
||||
Unit tests for stock location tree structure (MPTT).
|
||||
"""Unit tests for stock location tree structure (MPTT).
|
||||
|
||||
Ensure that the MPTT structure is rebuilt correctly,
|
||||
and the corrent ancestor tree is observed.
|
||||
|
||||
@ -686,9 +669,7 @@ class StockTest(InvenTreeTestCase):
|
||||
|
||||
|
||||
class VariantTest(StockTest):
|
||||
"""
|
||||
Tests for calculation stock counts against templates / variants
|
||||
"""
|
||||
"""Tests for calculation stock counts against templates / variants"""
|
||||
|
||||
def test_variant_stock(self):
|
||||
# Check the 'Chair' variant
|
||||
@ -769,9 +750,7 @@ class VariantTest(StockTest):
|
||||
|
||||
|
||||
class TestResultTest(StockTest):
|
||||
"""
|
||||
Tests for the StockItemTestResult model.
|
||||
"""
|
||||
"""Tests for the StockItemTestResult model."""
|
||||
|
||||
def test_test_count(self):
|
||||
item = StockItem.objects.get(pk=105)
|
||||
@ -898,12 +877,10 @@ class TestResultTest(StockTest):
|
||||
self.assertEqual(item3.test_results.count(), 4)
|
||||
|
||||
def test_installed_tests(self):
|
||||
"""
|
||||
Test test results for stock in stock.
|
||||
"""Test test results for stock in stock.
|
||||
|
||||
Or, test "test results" for "stock items" installed "inside" a "stock item"
|
||||
"""
|
||||
|
||||
# Get a "master" stock item
|
||||
item = StockItem.objects.get(pk=105)
|
||||
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
URL lookup for Stock app
|
||||
"""
|
||||
"""URL lookup for Stock app"""
|
||||
|
||||
from django.urls import include, re_path
|
||||
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
Django views for interacting with Stock app
|
||||
"""
|
||||
"""Django views for interacting with Stock app"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
@ -21,8 +19,7 @@ from .models import StockItem, StockItemTracking, StockLocation
|
||||
|
||||
|
||||
class StockIndex(InvenTreeRoleMixin, InvenTreePluginViewMixin, ListView):
|
||||
""" StockIndex view loads all StockLocation and StockItem object
|
||||
"""
|
||||
"""StockIndex view loads all StockLocation and StockItem object"""
|
||||
model = StockItem
|
||||
template_name = 'stock/location.html'
|
||||
context_obect_name = 'locations'
|
||||
@ -48,9 +45,7 @@ class StockIndex(InvenTreeRoleMixin, InvenTreePluginViewMixin, ListView):
|
||||
|
||||
|
||||
class StockLocationDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
|
||||
"""
|
||||
Detailed view of a single StockLocation object
|
||||
"""
|
||||
"""Detailed view of a single StockLocation object"""
|
||||
|
||||
context_object_name = 'location'
|
||||
template_name = 'stock/location.html'
|
||||
@ -69,9 +64,7 @@ class StockLocationDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailVi
|
||||
|
||||
|
||||
class StockItemDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
|
||||
"""
|
||||
Detailed view of a single StockItem object
|
||||
"""
|
||||
"""Detailed view of a single StockItem object"""
|
||||
|
||||
context_object_name = 'item'
|
||||
template_name = 'stock/item.html'
|
||||
@ -103,7 +96,7 @@ class StockItemDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
|
||||
return data
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
""" check if item exists else return to stock index """
|
||||
"""Check if item exists else return to stock index"""
|
||||
|
||||
stock_pk = kwargs.get('pk', None)
|
||||
|
||||
@ -120,7 +113,7 @@ class StockItemDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
|
||||
|
||||
|
||||
class StockLocationQRCode(QRCodeView):
|
||||
""" View for displaying a QR code for a StockLocation object """
|
||||
"""View for displaying a QR code for a StockLocation object"""
|
||||
|
||||
ajax_form_title = _("Stock Location QR code")
|
||||
|
||||
@ -136,9 +129,7 @@ class StockLocationQRCode(QRCodeView):
|
||||
|
||||
|
||||
class StockItemReturnToStock(AjaxUpdateView):
|
||||
"""
|
||||
View for returning a stock item (which is assigned to a customer) to stock.
|
||||
"""
|
||||
"""View for returning a stock item (which is assigned to a customer) to stock."""
|
||||
|
||||
model = StockItem
|
||||
ajax_form_title = _("Return to Stock")
|
||||
@ -166,9 +157,7 @@ class StockItemReturnToStock(AjaxUpdateView):
|
||||
|
||||
|
||||
class StockItemDeleteTestData(AjaxUpdateView):
|
||||
"""
|
||||
View for deleting all test data
|
||||
"""
|
||||
"""View for deleting all test data"""
|
||||
|
||||
model = StockItem
|
||||
form_class = ConfirmForm
|
||||
@ -203,7 +192,7 @@ class StockItemDeleteTestData(AjaxUpdateView):
|
||||
|
||||
|
||||
class StockItemQRCode(QRCodeView):
|
||||
""" View for displaying a QR code for a StockItem object """
|
||||
"""View for displaying a QR code for a StockItem object"""
|
||||
|
||||
ajax_form_title = _("Stock Item QR Code")
|
||||
role_required = 'stock.view'
|
||||
@ -218,9 +207,7 @@ class StockItemQRCode(QRCodeView):
|
||||
|
||||
|
||||
class StockItemConvert(AjaxUpdateView):
|
||||
"""
|
||||
View for 'converting' a StockItem to a variant of its current part.
|
||||
"""
|
||||
"""View for 'converting' a StockItem to a variant of its current part."""
|
||||
|
||||
model = StockItem
|
||||
form_class = StockForms.ConvertStockItemForm
|
||||
@ -229,9 +216,7 @@ class StockItemConvert(AjaxUpdateView):
|
||||
context_object_name = 'item'
|
||||
|
||||
def get_form(self):
|
||||
"""
|
||||
Filter the available parts.
|
||||
"""
|
||||
"""Filter the available parts."""
|
||||
|
||||
form = super().get_form()
|
||||
item = self.get_object()
|
||||
@ -289,7 +274,7 @@ class StockItemTrackingDelete(AjaxDeleteView):
|
||||
|
||||
|
||||
class StockItemTrackingEdit(AjaxUpdateView):
|
||||
""" View for editing a StockItemTracking object """
|
||||
"""View for editing a StockItemTracking object"""
|
||||
|
||||
model = StockItemTracking
|
||||
ajax_form_title = _('Edit Stock Tracking Entry')
|
||||
@ -297,8 +282,7 @@ class StockItemTrackingEdit(AjaxUpdateView):
|
||||
|
||||
|
||||
class StockItemTrackingCreate(AjaxCreateView):
|
||||
""" View for creating a new StockItemTracking object.
|
||||
"""
|
||||
"""View for creating a new StockItemTracking object."""
|
||||
|
||||
model = StockItemTracking
|
||||
ajax_form_title = _("Add Stock Tracking Entry")
|
||||
|
@ -14,9 +14,7 @@ User = get_user_model()
|
||||
|
||||
|
||||
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
|
||||
can_delete = False
|
||||
@ -76,9 +74,7 @@ class InvenTreeGroupAdminForm(forms.ModelForm):
|
||||
|
||||
|
||||
class RoleGroupAdmin(admin.ModelAdmin): # pragma: no cover
|
||||
"""
|
||||
Custom admin interface for the Group model
|
||||
"""
|
||||
"""Custom admin interface for the Group model"""
|
||||
|
||||
form = InvenTreeGroupAdminForm
|
||||
|
||||
@ -213,9 +209,7 @@ class InvenTreeUserAdmin(UserAdmin):
|
||||
|
||||
|
||||
class OwnerAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Custom admin interface for the Owner model
|
||||
"""
|
||||
"""Custom admin interface for the Owner model"""
|
||||
pass
|
||||
|
||||
|
||||
|
@ -14,9 +14,7 @@ from users.serializers import OwnerSerializer, UserSerializer
|
||||
|
||||
|
||||
class OwnerList(generics.ListAPIView):
|
||||
"""
|
||||
List API endpoint for Owner model. Cannot create.
|
||||
"""
|
||||
"""List API endpoint for Owner model. Cannot create."""
|
||||
|
||||
queryset = Owner.objects.all()
|
||||
serializer_class = OwnerSerializer
|
||||
@ -54,9 +52,7 @@ class OwnerList(generics.ListAPIView):
|
||||
|
||||
|
||||
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()
|
||||
serializer_class = OwnerSerializer
|
||||
@ -108,7 +104,7 @@ class RoleDetails(APIView):
|
||||
|
||||
|
||||
class UserDetail(generics.RetrieveAPIView):
|
||||
""" Detail endpoint for a single user """
|
||||
"""Detail endpoint for a single user"""
|
||||
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserSerializer
|
||||
@ -116,7 +112,7 @@ class UserDetail(generics.RetrieveAPIView):
|
||||
|
||||
|
||||
class UserList(generics.ListAPIView):
|
||||
""" List endpoint for detail on all users """
|
||||
"""List endpoint for detail on all users"""
|
||||
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserSerializer
|
||||
@ -135,7 +131,7 @@ class UserList(generics.ListAPIView):
|
||||
|
||||
|
||||
class GetAuthToken(APIView):
|
||||
""" Return authentication token for an authenticated user. """
|
||||
"""Return authentication token for an authenticated user."""
|
||||
|
||||
permission_classes = [
|
||||
permissions.IsAuthenticated,
|
||||
|
@ -221,9 +221,7 @@ class RuleSet(models.Model):
|
||||
|
||||
@classmethod
|
||||
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 table in cls.RULESET_IGNORE:
|
||||
@ -269,7 +267,7 @@ class RuleSet(models.Model):
|
||||
)
|
||||
|
||||
def __str__(self, debug=False): # pragma: no cover
|
||||
""" Ruleset string representation """
|
||||
"""Ruleset string representation"""
|
||||
if debug:
|
||||
# Makes debugging easier
|
||||
return f'{str(self.group).ljust(15)}: {self.name.title().ljust(15)} | ' \
|
||||
@ -296,15 +294,13 @@ class RuleSet(models.Model):
|
||||
self.group.save()
|
||||
|
||||
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, [])
|
||||
|
||||
|
||||
def split_model(model):
|
||||
"""get modelname and app from modelstring"""
|
||||
"""Get modelname and app from modelstring"""
|
||||
*app, model = model.split('_')
|
||||
|
||||
# handle models that have
|
||||
@ -317,7 +313,7 @@ def split_model(model):
|
||||
|
||||
|
||||
def split_permission(app, perm):
|
||||
"""split permission string into permission and model"""
|
||||
"""Split permission string into permission and model"""
|
||||
permission_name, *model = perm.split('_')
|
||||
# handle models that have underscores
|
||||
if len(model) > 1: # pragma: no cover
|
||||
@ -329,7 +325,6 @@ def split_permission(app, perm):
|
||||
|
||||
def update_group_roles(group, debug=False):
|
||||
"""
|
||||
|
||||
Iterates through all of the RuleSets associated with 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
|
||||
|
||||
The RuleSet model has complete control over the permissions applied to any group.
|
||||
|
||||
"""
|
||||
|
||||
if not canAppAccessDatabase(allow_test=True):
|
||||
@ -594,24 +588,20 @@ class Owner(models.Model):
|
||||
owner = GenericForeignKey('owner_type', 'owner_id')
|
||||
|
||||
def __str__(self):
|
||||
""" Defines the owner string representation """
|
||||
"""Defines the owner string representation"""
|
||||
return f'{self.owner} ({self.owner_type.name})'
|
||||
|
||||
def name(self):
|
||||
"""
|
||||
Return the 'name' of this owner
|
||||
"""
|
||||
"""Return the 'name' of this owner"""
|
||||
return str(self.owner)
|
||||
|
||||
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)
|
||||
|
||||
@classmethod
|
||||
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
|
||||
existing_owner = cls.get_owner(obj)
|
||||
@ -627,7 +617,7 @@ class Owner(models.Model):
|
||||
|
||||
@classmethod
|
||||
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()
|
||||
owner = None
|
||||
|
@ -10,8 +10,7 @@ from .models import Owner
|
||||
|
||||
|
||||
class UserSerializer(InvenTreeModelSerializer):
|
||||
""" Serializer for a User
|
||||
"""
|
||||
"""Serializer for a User"""
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
@ -23,9 +22,7 @@ class UserSerializer(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)
|
||||
|
||||
|
@ -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
|
||||
|
||||
@ -8,9 +6,7 @@ from InvenTree import helpers
|
||||
|
||||
|
||||
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_to = ('users', helpers.getNewestMigrationFile('users'))
|
||||
|
@ -10,9 +10,7 @@ from users.models import Owner, RuleSet
|
||||
|
||||
|
||||
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):
|
||||
|
||||
@ -48,10 +46,7 @@ class RuleSetModelTest(TestCase):
|
||||
self.assertEqual(len(empty), 0)
|
||||
|
||||
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()
|
||||
|
||||
@ -108,9 +103,7 @@ class RuleSetModelTest(TestCase):
|
||||
self.assertEqual(len(extra_models), 0)
|
||||
|
||||
def test_permission_assign(self):
|
||||
"""
|
||||
Test that the permission assigning works!
|
||||
"""
|
||||
"""Test that the permission assigning works!"""
|
||||
|
||||
# Create a new group
|
||||
group = Group.objects.create(name="Test group")
|
||||
@ -161,9 +154,7 @@ class RuleSetModelTest(TestCase):
|
||||
|
||||
|
||||
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):
|
||||
response = self.client.get(endpoint, filters, format='json')
|
||||
@ -212,9 +203,7 @@ class OwnerModelTest(InvenTreeTestCase):
|
||||
self.assertEqual(group_as_owner, None)
|
||||
|
||||
def test_api(self):
|
||||
"""
|
||||
Test user APIs
|
||||
"""
|
||||
"""Test user APIs"""
|
||||
self.client.logout()
|
||||
|
||||
# not authed
|
||||
@ -231,9 +220,7 @@ class OwnerModelTest(InvenTreeTestCase):
|
||||
# self.do_request(reverse('api-owner-detail', kwargs={'pk': self.user.id}), {})
|
||||
|
||||
def test_token(self):
|
||||
"""
|
||||
Test token mechanisms
|
||||
"""
|
||||
"""Test token mechanisms"""
|
||||
self.client.logout()
|
||||
|
||||
token = Token.objects.filter(user=self.user)
|
||||
|
Loading…
Reference in New Issue
Block a user