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