mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-17 12:35:46 +00:00
Docstring checks in QC checks (#3089)
* Add pre-commit to the stack * exclude static * Add locales to excludes * fix style errors * rename pipeline steps * also wait on precommit * make template matching simpler * Use the same code for python setup everywhere * use step and cache for python setup * move regular settings up into general envs * just use full update * Use invoke instead of static references * make setup actions more similar * use python3 * refactor names to be similar * fix runner version * fix references * remove incidential change * use matrix for os * Github can't do this right now * ignore docstyle errors * Add seperate docstring test * update flake call * do not fail on docstring * refactor setup into workflow * update reference * switch to action * resturcture * add bash statements * remove os from cache * update input checks * make code cleaner * fix boolean * no relative paths * install wheel by python * switch to install * revert back to simple wheel * refactor import export tests * move setup keys back to not disturbe tests * remove docstyle till that is fixed * update references * continue on error * add docstring test * use relativ action references * Change step / job docstrings * update to merge * reformat comments 1 * fix docstrings 2 * fix docstrings 3 * fix docstrings 4 * fix docstrings 5 * fix docstrings 6 * fix docstrings 7 * fix docstrings 8 * fix docstirns 9 * fix docstrings 10 * docstring adjustments * update the remaining docstrings * small docstring changes * fix function name * update support files for docstrings * Add missing args to docstrings * Remove outdated function * Add docstrings for the 'build' app * Make API code cleaner * add more docstrings for plugin app * Remove dead code for plugin settings No idea what that was even intended for * ignore __init__ files for docstrings * More docstrings * Update docstrings for the 'part' directory * Fixes for related_part functionality * Fix removed stuff from merge99676ee
* make more consistent * Show statistics for docstrings * add more docstrings * move specific register statements to make them clearer to understant * More docstrings for common * and more docstrings * and more * simpler call * docstrings for notifications * docstrings for common/tests * Add docs for common/models * Revert "move specific register statements to make them clearer to understant" This reverts commitca96654622
. * use typing here * Revert "Make API code cleaner" This reverts commit24fb68bd3e
. * docstring updates for the 'users' app * Add generic Meta info to simple Meta classes * remove unneeded unique_together statements * More simple metas * Remove unnecessary format specifier * Remove extra json format specifiers * Add docstrings for the 'plugin' app * Docstrings for the 'label' app * Add missing docstrings for the 'report' app * Fix build test regression * Fix top-level files * docstrings for InvenTree/InvenTree * reduce unneeded code * add docstrings * and more docstrings * more docstrings * more docstrings for stock * more docstrings * docstrings for order/views * Docstrings for various files in the 'order' app * Docstrings for order/test_api.py * Docstrings for order/serializers.py * Docstrings for order/admin.py * More docstrings for the order app * Add docstrings for the 'company' app * Add unit tests for rebuilding the reference fields * Prune out some more dead code * remove more dead code Co-authored-by: Oliver Walters <oliver.henry.walters@gmail.com>
This commit is contained in:
@ -1,5 +1,4 @@
|
||||
"""
|
||||
The Build module is responsible for managing "Build" transactions.
|
||||
"""The Build module is responsible for managing "Build" transactions.
|
||||
|
||||
A Build consumes parts from stock to create new parts
|
||||
"""
|
||||
|
@ -1,3 +1,5 @@
|
||||
"""Admin functionality for the BuildOrder app"""
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from import_export.admin import ImportExportModelAdmin
|
||||
@ -11,7 +13,7 @@ import part.models
|
||||
|
||||
|
||||
class BuildResource(ModelResource):
|
||||
"""Class for managing import/export of Build data"""
|
||||
"""Class for managing import/export of Build data."""
|
||||
# For some reason, we need to specify the fields individually for this ModelResource,
|
||||
# but we don't for other ones.
|
||||
# TODO: 2022-05-12 - Need to investigate why this is the case!
|
||||
@ -39,6 +41,7 @@ class BuildResource(ModelResource):
|
||||
notes = Field(attribute='notes')
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options"""
|
||||
models = Build
|
||||
skip_unchanged = True
|
||||
report_skipped = False
|
||||
@ -50,6 +53,7 @@ class BuildResource(ModelResource):
|
||||
|
||||
|
||||
class BuildAdmin(ImportExportModelAdmin):
|
||||
"""Class for managing the Build model via the admin interface"""
|
||||
|
||||
exclude = [
|
||||
'reference_int',
|
||||
@ -81,6 +85,7 @@ class BuildAdmin(ImportExportModelAdmin):
|
||||
|
||||
|
||||
class BuildItemAdmin(admin.ModelAdmin):
|
||||
"""Class for managing the BuildItem model via the admin interface"""
|
||||
|
||||
list_display = (
|
||||
'build',
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
JSON API for the Build app
|
||||
"""
|
||||
"""JSON API for the Build app."""
|
||||
|
||||
from django.urls import include, re_path
|
||||
|
||||
@ -22,16 +20,14 @@ from users.models import Owner
|
||||
|
||||
|
||||
class BuildFilter(rest_filters.FilterSet):
|
||||
"""
|
||||
Custom filterset for BuildList API endpoint
|
||||
"""
|
||||
"""Custom filterset for BuildList API endpoint."""
|
||||
|
||||
status = rest_filters.NumberFilter(label='Status')
|
||||
|
||||
active = rest_filters.BooleanFilter(label='Build is active', method='filter_active')
|
||||
|
||||
def filter_active(self, queryset, name, value):
|
||||
|
||||
"""Filter the queryset to either include or exclude orders which are active."""
|
||||
if str2bool(value):
|
||||
queryset = queryset.filter(status__in=BuildStatus.ACTIVE_CODES)
|
||||
else:
|
||||
@ -42,7 +38,7 @@ class BuildFilter(rest_filters.FilterSet):
|
||||
overdue = rest_filters.BooleanFilter(label='Build is overdue', method='filter_overdue')
|
||||
|
||||
def filter_overdue(self, queryset, name, value):
|
||||
|
||||
"""Filter the queryset to either include or exclude orders which are overdue."""
|
||||
if str2bool(value):
|
||||
queryset = queryset.filter(Build.OVERDUE_FILTER)
|
||||
else:
|
||||
@ -53,10 +49,7 @@ class BuildFilter(rest_filters.FilterSet):
|
||||
assigned_to_me = rest_filters.BooleanFilter(label='assigned_to_me', method='filter_assigned_to_me')
|
||||
|
||||
def filter_assigned_to_me(self, queryset, name, value):
|
||||
"""
|
||||
Filter by orders which are assigned to the current user
|
||||
"""
|
||||
|
||||
"""Filter by orders which are assigned to the current user."""
|
||||
value = str2bool(value)
|
||||
|
||||
# Work out who "me" is!
|
||||
@ -71,7 +64,7 @@ class BuildFilter(rest_filters.FilterSet):
|
||||
|
||||
|
||||
class BuildList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
""" API endpoint for accessing a list of Build objects.
|
||||
"""API endpoint for accessing a list of Build objects.
|
||||
|
||||
- GET: Return list of objects (with filters)
|
||||
- POST: Create a new Build object
|
||||
@ -113,11 +106,7 @@ class BuildList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Override the queryset filtering,
|
||||
as some of the fields don't natively play nicely with DRF
|
||||
"""
|
||||
|
||||
"""Override the queryset filtering, as some of the fields don't natively play nicely with DRF."""
|
||||
queryset = super().get_queryset().select_related('part')
|
||||
|
||||
queryset = build.serializers.BuildSerializer.annotate_queryset(queryset)
|
||||
@ -125,6 +114,7 @@ class BuildList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
return queryset
|
||||
|
||||
def download_queryset(self, queryset, export_format):
|
||||
"""Download the queryset data as a file."""
|
||||
dataset = build.admin.BuildResource().export(queryset=queryset)
|
||||
|
||||
filedata = dataset.export(export_format)
|
||||
@ -133,7 +123,7 @@ class BuildList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
return DownloadFile(filedata, filename)
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
|
||||
"""Custom query filtering for the BuildList endpoint."""
|
||||
queryset = super().filter_queryset(queryset)
|
||||
|
||||
params = self.request.query_params
|
||||
@ -197,7 +187,7 @@ class BuildList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
return queryset
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
|
||||
"""Add extra context information to the endpoint serializer."""
|
||||
try:
|
||||
part_detail = str2bool(self.request.GET.get('part_detail', None))
|
||||
except AttributeError:
|
||||
@ -209,15 +199,14 @@ class BuildList(APIDownloadMixin, generics.ListCreateAPIView):
|
||||
|
||||
|
||||
class BuildDetail(generics.RetrieveUpdateAPIView):
|
||||
""" API endpoint for detail view of a Build object """
|
||||
"""API endpoint for detail view of a Build object."""
|
||||
|
||||
queryset = Build.objects.all()
|
||||
serializer_class = build.serializers.BuildSerializer
|
||||
|
||||
|
||||
class BuildUnallocate(generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint for unallocating stock items from a build order
|
||||
"""API endpoint for unallocating stock items from a build order.
|
||||
|
||||
- The BuildOrder object is specified by the URL
|
||||
- "output" (StockItem) can optionally be specified
|
||||
@ -229,7 +218,7 @@ class BuildUnallocate(generics.CreateAPIView):
|
||||
serializer_class = build.serializers.BuildUnallocationSerializer
|
||||
|
||||
def get_serializer_context(self):
|
||||
|
||||
"""Add extra context information to the endpoint serializer."""
|
||||
ctx = super().get_serializer_context()
|
||||
|
||||
try:
|
||||
@ -243,9 +232,10 @@ class BuildUnallocate(generics.CreateAPIView):
|
||||
|
||||
|
||||
class BuildOrderContextMixin:
|
||||
""" Mixin class which adds build order as serializer context variable """
|
||||
"""Mixin class which adds build order as serializer context variable."""
|
||||
|
||||
def get_serializer_context(self):
|
||||
"""Add extra context information to the endpoint serializer."""
|
||||
ctx = super().get_serializer_context()
|
||||
|
||||
ctx['request'] = self.request
|
||||
@ -260,9 +250,7 @@ class BuildOrderContextMixin:
|
||||
|
||||
|
||||
class BuildOutputCreate(BuildOrderContextMixin, generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint for creating new build output(s)
|
||||
"""
|
||||
"""API endpoint for creating new build output(s)."""
|
||||
|
||||
queryset = Build.objects.none()
|
||||
|
||||
@ -270,9 +258,7 @@ class BuildOutputCreate(BuildOrderContextMixin, generics.CreateAPIView):
|
||||
|
||||
|
||||
class BuildOutputComplete(BuildOrderContextMixin, generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint for completing build outputs
|
||||
"""
|
||||
"""API endpoint for completing build outputs."""
|
||||
|
||||
queryset = Build.objects.none()
|
||||
|
||||
@ -280,11 +266,10 @@ class BuildOutputComplete(BuildOrderContextMixin, generics.CreateAPIView):
|
||||
|
||||
|
||||
class BuildOutputDelete(BuildOrderContextMixin, generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint for deleting multiple build outputs
|
||||
"""
|
||||
"""API endpoint for deleting multiple build outputs."""
|
||||
|
||||
def get_serializer_context(self):
|
||||
"""Add extra context information to the endpoint serializer."""
|
||||
ctx = super().get_serializer_context()
|
||||
|
||||
ctx['to_complete'] = False
|
||||
@ -297,9 +282,7 @@ class BuildOutputDelete(BuildOrderContextMixin, generics.CreateAPIView):
|
||||
|
||||
|
||||
class BuildFinish(BuildOrderContextMixin, generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint for marking a build as finished (completed)
|
||||
"""
|
||||
"""API endpoint for marking a build as finished (completed)."""
|
||||
|
||||
queryset = Build.objects.none()
|
||||
|
||||
@ -307,8 +290,7 @@ class BuildFinish(BuildOrderContextMixin, generics.CreateAPIView):
|
||||
|
||||
|
||||
class BuildAutoAllocate(BuildOrderContextMixin, generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint for 'automatically' allocating stock against a build order.
|
||||
"""API endpoint for 'automatically' allocating stock against a build order.
|
||||
|
||||
- Only looks at 'untracked' parts
|
||||
- If stock exists in a single location, easy!
|
||||
@ -322,8 +304,7 @@ class BuildAutoAllocate(BuildOrderContextMixin, generics.CreateAPIView):
|
||||
|
||||
|
||||
class BuildAllocate(BuildOrderContextMixin, generics.CreateAPIView):
|
||||
"""
|
||||
API endpoint to allocate stock items to a build order
|
||||
"""API endpoint to allocate stock items to a build order.
|
||||
|
||||
- The BuildOrder object is specified by the URL
|
||||
- Items to allocate are specified as a list called "items" with the following options:
|
||||
@ -339,23 +320,21 @@ class BuildAllocate(BuildOrderContextMixin, generics.CreateAPIView):
|
||||
|
||||
|
||||
class BuildCancel(BuildOrderContextMixin, generics.CreateAPIView):
|
||||
""" API endpoint for cancelling a BuildOrder """
|
||||
"""API endpoint for cancelling a BuildOrder."""
|
||||
|
||||
queryset = Build.objects.all()
|
||||
serializer_class = build.serializers.BuildCancelSerializer
|
||||
|
||||
|
||||
class BuildItemDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
API endpoint for detail view of a BuildItem object
|
||||
"""
|
||||
"""API endpoint for detail view of a BuildItem object."""
|
||||
|
||||
queryset = BuildItem.objects.all()
|
||||
serializer_class = build.serializers.BuildItemSerializer
|
||||
|
||||
|
||||
class BuildItemList(generics.ListCreateAPIView):
|
||||
""" API endpoint for accessing a list of BuildItem objects
|
||||
"""API endpoint for accessing a list of BuildItem objects.
|
||||
|
||||
- GET: Return list of objects
|
||||
- POST: Create a new BuildItem object
|
||||
@ -364,7 +343,7 @@ class BuildItemList(generics.ListCreateAPIView):
|
||||
serializer_class = build.serializers.BuildItemSerializer
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
|
||||
"""Returns a BuildItemSerializer instance based on the request."""
|
||||
try:
|
||||
params = self.request.query_params
|
||||
|
||||
@ -377,10 +356,7 @@ class BuildItemList(generics.ListCreateAPIView):
|
||||
return self.serializer_class(*args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
""" Override the queryset method,
|
||||
to allow filtering by stock_item.part
|
||||
"""
|
||||
|
||||
"""Override the queryset method, to allow filtering by stock_item.part."""
|
||||
query = BuildItem.objects.all()
|
||||
|
||||
query = query.select_related('stock_item__location')
|
||||
@ -390,7 +366,7 @@ class BuildItemList(generics.ListCreateAPIView):
|
||||
return query
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
|
||||
"""Customm query filtering for the BuildItem list."""
|
||||
queryset = super().filter_queryset(queryset)
|
||||
|
||||
params = self.request.query_params
|
||||
@ -438,9 +414,7 @@ class BuildItemList(generics.ListCreateAPIView):
|
||||
|
||||
|
||||
class BuildAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
||||
"""
|
||||
API endpoint for listing (and creating) BuildOrderAttachment objects
|
||||
"""
|
||||
"""API endpoint for listing (and creating) BuildOrderAttachment objects."""
|
||||
|
||||
queryset = BuildOrderAttachment.objects.all()
|
||||
serializer_class = build.serializers.BuildAttachmentSerializer
|
||||
@ -455,9 +429,7 @@ class BuildAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
||||
|
||||
|
||||
class BuildAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin):
|
||||
"""
|
||||
Detail endpoint for a BuildOrderAttachment object
|
||||
"""
|
||||
"""Detail endpoint for a BuildOrderAttachment object."""
|
||||
|
||||
queryset = BuildOrderAttachment.objects.all()
|
||||
serializer_class = build.serializers.BuildAttachmentSerializer
|
||||
|
@ -1,5 +1,8 @@
|
||||
"""Django app for the BuildOrder module"""
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BuildConfig(AppConfig):
|
||||
"""BuildOrder app config class"""
|
||||
name = 'build'
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
Build database model definitions
|
||||
"""
|
||||
"""Build database model definitions."""
|
||||
|
||||
import decimal
|
||||
|
||||
@ -42,10 +40,7 @@ from users import models as UserModels
|
||||
|
||||
|
||||
def get_next_build_number():
|
||||
"""
|
||||
Returns the next available BuildOrder reference number
|
||||
"""
|
||||
|
||||
"""Returns the next available BuildOrder reference number."""
|
||||
if Build.objects.count() == 0:
|
||||
return '0001'
|
||||
|
||||
@ -71,7 +66,7 @@ def get_next_build_number():
|
||||
|
||||
|
||||
class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
""" A Build object organises the creation of new StockItem objects from other existing StockItem objects.
|
||||
"""A Build object organises the creation of new StockItem objects from other existing StockItem objects.
|
||||
|
||||
Attributes:
|
||||
part: The part to be built (from component BOM items)
|
||||
@ -97,10 +92,11 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL associated with the BuildOrder model"""
|
||||
return reverse('api-build-list')
|
||||
|
||||
def api_instance_filters(self):
|
||||
|
||||
"""Returns custom API filters for the particular BuildOrder instance"""
|
||||
return {
|
||||
'parent': {
|
||||
'exclude_tree': self.pk,
|
||||
@ -109,10 +105,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
|
||||
@classmethod
|
||||
def api_defaults(cls, request):
|
||||
"""
|
||||
Return default values for this model when issuing an API OPTIONS request
|
||||
"""
|
||||
|
||||
"""Return default values for this model when issuing an API OPTIONS request."""
|
||||
defaults = {
|
||||
'reference': get_next_build_number(),
|
||||
}
|
||||
@ -123,7 +116,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
return defaults
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
"""Custom save method for the BuildOrder model"""
|
||||
self.rebuild_reference_field()
|
||||
|
||||
try:
|
||||
@ -134,14 +127,12 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
})
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options for the BuildOrder model"""
|
||||
verbose_name = _("Build Order")
|
||||
verbose_name_plural = _("Build Orders")
|
||||
|
||||
def format_barcode(self, **kwargs):
|
||||
"""
|
||||
Return a JSON string to represent this build as a barcode
|
||||
"""
|
||||
|
||||
"""Return a JSON string to represent this build as a barcode."""
|
||||
return MakeBarcode(
|
||||
"buildorder",
|
||||
self.pk,
|
||||
@ -153,13 +144,11 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
|
||||
@staticmethod
|
||||
def filterByDate(queryset, min_date, max_date):
|
||||
"""
|
||||
Filter by 'minimum and maximum date range'
|
||||
"""Filter by 'minimum and maximum date range'.
|
||||
|
||||
- Specified as min_date, max_date
|
||||
- Both must be specified for filter to be applied
|
||||
"""
|
||||
|
||||
date_fmt = '%Y-%m-%d' # ISO format date string
|
||||
|
||||
# Ensure that both dates are valid
|
||||
@ -183,12 +172,13 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
return queryset
|
||||
|
||||
def __str__(self):
|
||||
|
||||
"""String representation of a BuildOrder"""
|
||||
prefix = getSetting("BUILDORDER_REFERENCE_PREFIX")
|
||||
|
||||
return f"{prefix}{self.reference}"
|
||||
|
||||
def get_absolute_url(self):
|
||||
"""Return the web URL associated with this BuildOrder"""
|
||||
return reverse('build-detail', kwargs={'pk': self.id})
|
||||
|
||||
reference = models.CharField(
|
||||
@ -336,10 +326,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
)
|
||||
|
||||
def sub_builds(self, cascade=True):
|
||||
"""
|
||||
Return all Build Order objects under this one.
|
||||
"""
|
||||
|
||||
"""Return all Build Order objects under this one."""
|
||||
if cascade:
|
||||
return Build.objects.filter(parent=self.pk)
|
||||
else:
|
||||
@ -347,23 +334,22 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
Build.objects.filter(parent__pk__in=[d.pk for d in descendants])
|
||||
|
||||
def sub_build_count(self, cascade=True):
|
||||
"""
|
||||
Return the number of sub builds under this one.
|
||||
"""Return the number of sub builds under this one.
|
||||
|
||||
Args:
|
||||
cascade: If True (defualt), include cascading builds under sub builds
|
||||
"""
|
||||
|
||||
return self.sub_builds(cascade=cascade).count()
|
||||
|
||||
@property
|
||||
def is_overdue(self):
|
||||
"""
|
||||
Returns true if this build is "overdue":
|
||||
"""Returns true if this build is "overdue".
|
||||
|
||||
Makes use of the OVERDUE_FILTER to avoid code duplication
|
||||
"""
|
||||
|
||||
Returns:
|
||||
bool: Is the build overdue
|
||||
"""
|
||||
query = Build.objects.filter(pk=self.pk)
|
||||
query = query.filter(Build.OVERDUE_FILTER)
|
||||
|
||||
@ -371,80 +357,59 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
|
||||
@property
|
||||
def active(self):
|
||||
"""
|
||||
Return True if this build is active
|
||||
"""
|
||||
|
||||
"""Return True if this build is active."""
|
||||
return self.status in BuildStatus.ACTIVE_CODES
|
||||
|
||||
@property
|
||||
def bom_items(self):
|
||||
"""
|
||||
Returns the BOM items for the part referenced by this BuildOrder
|
||||
"""
|
||||
|
||||
"""Returns the BOM items for the part referenced by this BuildOrder."""
|
||||
return self.part.get_bom_items()
|
||||
|
||||
@property
|
||||
def tracked_bom_items(self):
|
||||
"""
|
||||
Returns the "trackable" BOM items for this BuildOrder
|
||||
"""
|
||||
|
||||
"""Returns the "trackable" BOM items for this BuildOrder."""
|
||||
items = self.bom_items
|
||||
items = items.filter(sub_part__trackable=True)
|
||||
|
||||
return items
|
||||
|
||||
def has_tracked_bom_items(self):
|
||||
"""
|
||||
Returns True if this BuildOrder has trackable BomItems
|
||||
"""
|
||||
|
||||
"""Returns True if this BuildOrder has trackable BomItems."""
|
||||
return self.tracked_bom_items.count() > 0
|
||||
|
||||
@property
|
||||
def untracked_bom_items(self):
|
||||
"""
|
||||
Returns the "non trackable" BOM items for this BuildOrder
|
||||
"""
|
||||
|
||||
"""Returns the "non trackable" BOM items for this BuildOrder."""
|
||||
items = self.bom_items
|
||||
items = items.filter(sub_part__trackable=False)
|
||||
|
||||
return items
|
||||
|
||||
def has_untracked_bom_items(self):
|
||||
"""
|
||||
Returns True if this BuildOrder has non trackable BomItems
|
||||
"""
|
||||
|
||||
"""Returns True if this BuildOrder has non trackable BomItems."""
|
||||
return self.untracked_bom_items.count() > 0
|
||||
|
||||
@property
|
||||
def remaining(self):
|
||||
"""
|
||||
Return the number of outputs remaining to be completed.
|
||||
"""
|
||||
|
||||
"""Return the number of outputs remaining to be completed."""
|
||||
return max(0, self.quantity - self.completed)
|
||||
|
||||
@property
|
||||
def output_count(self):
|
||||
"""Return the number of build outputs (StockItem) associated with this build order"""
|
||||
return self.build_outputs.count()
|
||||
|
||||
def has_build_outputs(self):
|
||||
"""Returns True if this build has more than zero build outputs"""
|
||||
return self.output_count > 0
|
||||
|
||||
def get_build_outputs(self, **kwargs):
|
||||
"""
|
||||
Return a list of build outputs.
|
||||
"""Return a list of build outputs.
|
||||
|
||||
kwargs:
|
||||
complete = (True / False) - If supplied, filter by completed status
|
||||
in_stock = (True / False) - If supplied, filter by 'in-stock' status
|
||||
"""
|
||||
|
||||
outputs = self.build_outputs.all()
|
||||
|
||||
# Filter by 'in stock' status
|
||||
@ -469,17 +434,14 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
|
||||
@property
|
||||
def complete_outputs(self):
|
||||
"""
|
||||
Return all the "completed" build outputs
|
||||
"""
|
||||
|
||||
"""Return all the "completed" build outputs."""
|
||||
outputs = self.get_build_outputs(complete=True)
|
||||
|
||||
return outputs
|
||||
|
||||
@property
|
||||
def complete_count(self):
|
||||
|
||||
"""Return the total quantity of completed outputs"""
|
||||
quantity = 0
|
||||
|
||||
for output in self.complete_outputs:
|
||||
@ -489,20 +451,14 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
|
||||
@property
|
||||
def incomplete_outputs(self):
|
||||
"""
|
||||
Return all the "incomplete" build outputs
|
||||
"""
|
||||
|
||||
"""Return all the "incomplete" build outputs."""
|
||||
outputs = self.get_build_outputs(complete=False)
|
||||
|
||||
return outputs
|
||||
|
||||
@property
|
||||
def incomplete_count(self):
|
||||
"""
|
||||
Return the total number of "incomplete" outputs
|
||||
"""
|
||||
|
||||
"""Return the total number of "incomplete" outputs."""
|
||||
quantity = 0
|
||||
|
||||
for output in self.incomplete_outputs:
|
||||
@ -512,10 +468,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
|
||||
@classmethod
|
||||
def getNextBuildNumber(cls):
|
||||
"""
|
||||
Try to predict the next Build Order reference:
|
||||
"""
|
||||
|
||||
"""Try to predict the next Build Order reference."""
|
||||
if cls.objects.count() == 0:
|
||||
return None
|
||||
|
||||
@ -552,13 +505,11 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
|
||||
@property
|
||||
def can_complete(self):
|
||||
"""
|
||||
Returns True if this build can be "completed"
|
||||
"""Returns True if this build can be "completed".
|
||||
|
||||
- Must not have any outstanding build outputs
|
||||
- 'completed' value must meet (or exceed) the 'quantity' value
|
||||
"""
|
||||
|
||||
if self.incomplete_count > 0:
|
||||
return False
|
||||
|
||||
@ -573,10 +524,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
|
||||
@transaction.atomic
|
||||
def complete_build(self, user):
|
||||
"""
|
||||
Mark this build as complete
|
||||
"""
|
||||
|
||||
"""Mark this build as complete."""
|
||||
if self.incomplete_count > 0:
|
||||
return
|
||||
|
||||
@ -597,13 +545,12 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
|
||||
@transaction.atomic
|
||||
def cancel_build(self, user, **kwargs):
|
||||
""" Mark the Build as CANCELLED
|
||||
"""Mark the Build as CANCELLED.
|
||||
|
||||
- Delete any pending BuildItem objects (but do not remove items from stock)
|
||||
- Set build status to CANCELLED
|
||||
- Save the Build object
|
||||
"""
|
||||
|
||||
remove_allocated_stock = kwargs.get('remove_allocated_stock', False)
|
||||
remove_incomplete_outputs = kwargs.get('remove_incomplete_outputs', False)
|
||||
|
||||
@ -633,14 +580,12 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
|
||||
@transaction.atomic
|
||||
def unallocateStock(self, bom_item=None, output=None):
|
||||
"""
|
||||
Unallocate stock from this Build
|
||||
"""Unallocate stock from this Build.
|
||||
|
||||
arguments:
|
||||
- bom_item: Specify a particular BomItem to unallocate stock against
|
||||
- output: Specify a particular StockItem (output) to unallocate stock against
|
||||
Args:
|
||||
bom_item: Specify a particular BomItem to unallocate stock against
|
||||
output: Specify a particular StockItem (output) to unallocate stock against
|
||||
"""
|
||||
|
||||
allocations = BuildItem.objects.filter(
|
||||
build=self,
|
||||
install_into=output
|
||||
@ -653,19 +598,17 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
|
||||
@transaction.atomic
|
||||
def create_build_output(self, quantity, **kwargs):
|
||||
"""
|
||||
Create a new build output against this BuildOrder.
|
||||
"""Create a new build output against this BuildOrder.
|
||||
|
||||
args:
|
||||
Args:
|
||||
quantity: The quantity of the item to produce
|
||||
|
||||
kwargs:
|
||||
Kwargs:
|
||||
batch: Override batch code
|
||||
serials: Serial numbers
|
||||
location: Override location
|
||||
auto_allocate: Automatically allocate stock with matching serial numbers
|
||||
"""
|
||||
|
||||
batch = kwargs.get('batch', self.batch)
|
||||
location = kwargs.get('location', self.destination)
|
||||
serials = kwargs.get('serials', None)
|
||||
@ -687,9 +630,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
multiple = True
|
||||
|
||||
if multiple:
|
||||
"""
|
||||
Create multiple build outputs with a single quantity of 1
|
||||
"""
|
||||
"""Create multiple build outputs with a single quantity of 1."""
|
||||
|
||||
# Quantity *must* be an integer at this point!
|
||||
quantity = int(quantity)
|
||||
@ -743,9 +684,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
)
|
||||
|
||||
else:
|
||||
"""
|
||||
Create a single build output of the given quantity
|
||||
"""
|
||||
"""Create a single build output of the given quantity."""
|
||||
|
||||
StockModels.StockItem.objects.create(
|
||||
quantity=quantity,
|
||||
@ -762,13 +701,12 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
|
||||
@transaction.atomic
|
||||
def delete_output(self, output):
|
||||
"""
|
||||
Remove a build output from the database:
|
||||
"""Remove a build output from the database.
|
||||
|
||||
Executes:
|
||||
- Unallocate any build items against the output
|
||||
- Delete the output StockItem
|
||||
"""
|
||||
|
||||
if not output:
|
||||
raise ValidationError(_("No build output specified"))
|
||||
|
||||
@ -786,11 +724,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
|
||||
@transaction.atomic
|
||||
def subtract_allocated_stock(self, user):
|
||||
"""
|
||||
Called when the Build is marked as "complete",
|
||||
this function removes the allocated untracked items from stock.
|
||||
"""
|
||||
|
||||
"""Called when the Build is marked as "complete", this function removes the allocated untracked items from stock."""
|
||||
items = self.allocated_stock.filter(
|
||||
stock_item__part__trackable=False
|
||||
)
|
||||
@ -804,13 +738,11 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
|
||||
@transaction.atomic
|
||||
def complete_build_output(self, output, user, **kwargs):
|
||||
"""
|
||||
Complete a particular build output
|
||||
"""Complete a particular build output.
|
||||
|
||||
- Remove allocated StockItems
|
||||
- Mark the output as complete
|
||||
"""
|
||||
|
||||
# Select the location for the build output
|
||||
location = kwargs.get('location', self.destination)
|
||||
status = kwargs.get('status', StockStatus.OK)
|
||||
@ -850,10 +782,9 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
|
||||
@transaction.atomic
|
||||
def auto_allocate_stock(self, **kwargs):
|
||||
"""
|
||||
Automatically allocate stock items against this build order,
|
||||
following a number of 'guidelines':
|
||||
"""Automatically allocate stock items against this build order.
|
||||
|
||||
Following a number of 'guidelines':
|
||||
- Only "untracked" BOM items are considered (tracked BOM items must be manually allocated)
|
||||
- If a particular BOM item is already fully allocated, it is skipped
|
||||
- Extract all available stock items for the BOM part
|
||||
@ -863,7 +794,6 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
- If multiple stock items are found, we *may* be able to allocate:
|
||||
- If the calling function has specified that items are interchangeable
|
||||
"""
|
||||
|
||||
location = kwargs.get('location', None)
|
||||
exclude_location = kwargs.get('exclude_location', None)
|
||||
interchangeable = kwargs.get('interchangeable', False)
|
||||
@ -958,14 +888,12 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
break
|
||||
|
||||
def required_quantity(self, bom_item, output=None):
|
||||
"""
|
||||
Get the quantity of a part required to complete the particular build output.
|
||||
"""Get the quantity of a part required to complete the particular build output.
|
||||
|
||||
Args:
|
||||
part: The Part object
|
||||
output - The particular build output (StockItem)
|
||||
bom_item: The Part object
|
||||
output: The particular build output (StockItem)
|
||||
"""
|
||||
|
||||
quantity = bom_item.quantity
|
||||
|
||||
if output:
|
||||
@ -976,17 +904,15 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
return quantity
|
||||
|
||||
def allocated_bom_items(self, bom_item, output=None):
|
||||
"""
|
||||
Return all BuildItem objects which allocate stock of <bom_item> to <output>
|
||||
"""Return all BuildItem objects which allocate stock of <bom_item> to <output>.
|
||||
|
||||
Note that the bom_item may allow variants, or direct substitutes,
|
||||
making things difficult.
|
||||
|
||||
Args:
|
||||
bom_item - The BomItem object
|
||||
output - Build output (StockItem).
|
||||
bom_item: The BomItem object
|
||||
output: Build output (StockItem).
|
||||
"""
|
||||
|
||||
allocations = BuildItem.objects.filter(
|
||||
build=self,
|
||||
bom_item=bom_item,
|
||||
@ -996,10 +922,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
return allocations
|
||||
|
||||
def allocated_quantity(self, bom_item, output=None):
|
||||
"""
|
||||
Return the total quantity of given part allocated to a given build output.
|
||||
"""
|
||||
|
||||
"""Return the total quantity of given part allocated to a given build output."""
|
||||
allocations = self.allocated_bom_items(bom_item, output)
|
||||
|
||||
allocated = allocations.aggregate(
|
||||
@ -1013,27 +936,18 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
return allocated['q']
|
||||
|
||||
def unallocated_quantity(self, bom_item, output=None):
|
||||
"""
|
||||
Return the total unallocated (remaining) quantity of a part against a particular output.
|
||||
"""
|
||||
|
||||
"""Return the total unallocated (remaining) quantity of a part against a particular output."""
|
||||
required = self.required_quantity(bom_item, output)
|
||||
allocated = self.allocated_quantity(bom_item, output)
|
||||
|
||||
return max(required - allocated, 0)
|
||||
|
||||
def is_bom_item_allocated(self, bom_item, output=None):
|
||||
"""
|
||||
Test if the supplied BomItem has been fully allocated!
|
||||
"""
|
||||
|
||||
"""Test if the supplied BomItem has been fully allocated!"""
|
||||
return self.unallocated_quantity(bom_item, output) == 0
|
||||
|
||||
def is_fully_allocated(self, output):
|
||||
"""
|
||||
Returns True if the particular build output is fully allocated.
|
||||
"""
|
||||
|
||||
"""Returns True if the particular build output is fully allocated."""
|
||||
# If output is not specified, we are talking about "untracked" items
|
||||
if output is None:
|
||||
bom_items = self.untracked_bom_items
|
||||
@ -1049,10 +963,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
return True
|
||||
|
||||
def is_partially_allocated(self, output):
|
||||
"""
|
||||
Returns True if the particular build output is (at least) partially allocated
|
||||
"""
|
||||
|
||||
"""Returns True if the particular build output is (at least) partially allocated."""
|
||||
# If output is not specified, we are talking about "untracked" items
|
||||
if output is None:
|
||||
bom_items = self.untracked_bom_items
|
||||
@ -1067,17 +978,11 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
return False
|
||||
|
||||
def are_untracked_parts_allocated(self):
|
||||
"""
|
||||
Returns True if the un-tracked parts are fully allocated for this BuildOrder
|
||||
"""
|
||||
|
||||
"""Returns True if the un-tracked parts are fully allocated for this BuildOrder."""
|
||||
return self.is_fully_allocated(None)
|
||||
|
||||
def unallocated_bom_items(self, output):
|
||||
"""
|
||||
Return a list of bom items which have *not* been fully allocated against a particular output
|
||||
"""
|
||||
|
||||
"""Return a list of bom items which have *not* been fully allocated against a particular output."""
|
||||
unallocated = []
|
||||
|
||||
# If output is not specified, we are talking about "untracked" items
|
||||
@ -1095,7 +1000,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
|
||||
@property
|
||||
def required_parts(self):
|
||||
""" Returns a list of parts required to build this part (BOM) """
|
||||
"""Returns a list of parts required to build this part (BOM)."""
|
||||
parts = []
|
||||
|
||||
for item in self.bom_items:
|
||||
@ -1105,7 +1010,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
|
||||
@property
|
||||
def required_parts_to_complete_build(self):
|
||||
""" Returns a list of parts required to complete the full build """
|
||||
"""Returns a list of parts required to complete the full build."""
|
||||
parts = []
|
||||
|
||||
for bom_item in self.bom_items:
|
||||
@ -1119,26 +1024,23 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
""" Is this build active? An active build is either:
|
||||
"""Is this build active?
|
||||
|
||||
An active build is either:
|
||||
- PENDING
|
||||
- HOLDING
|
||||
"""
|
||||
|
||||
return self.status in BuildStatus.ACTIVE_CODES
|
||||
|
||||
@property
|
||||
def is_complete(self):
|
||||
""" Returns True if the build status is COMPLETE """
|
||||
|
||||
"""Returns True if the build status is COMPLETE."""
|
||||
return self.status == BuildStatus.COMPLETE
|
||||
|
||||
|
||||
@receiver(post_save, sender=Build, dispatch_uid='build_post_save_log')
|
||||
def after_save_build(sender, instance: Build, created: bool, **kwargs):
|
||||
"""
|
||||
Callback function to be executed after a Build instance is saved
|
||||
"""
|
||||
"""Callback function to be executed after a Build instance is saved."""
|
||||
from . import tasks as build_tasks
|
||||
|
||||
if created:
|
||||
@ -1149,21 +1051,19 @@ def after_save_build(sender, instance: Build, created: bool, **kwargs):
|
||||
|
||||
|
||||
class BuildOrderAttachment(InvenTreeAttachment):
|
||||
"""
|
||||
Model for storing file attachments against a BuildOrder object
|
||||
"""
|
||||
"""Model for storing file attachments against a BuildOrder object."""
|
||||
|
||||
def getSubdir(self):
|
||||
"""Return the media file subdirectory for storing BuildOrder attachments"""
|
||||
return os.path.join('bo_files', str(self.build.id))
|
||||
|
||||
build = models.ForeignKey(Build, on_delete=models.CASCADE, related_name='attachments')
|
||||
|
||||
|
||||
class BuildItem(models.Model):
|
||||
""" A BuildItem links multiple StockItem objects to a Build.
|
||||
These are used to allocate part stock to a build.
|
||||
Once the Build is completed, the parts are removed from stock and the
|
||||
BuildItemAllocation objects are removed.
|
||||
"""A BuildItem links multiple StockItem objects to a Build.
|
||||
|
||||
These are used to allocate part stock to a build. Once the Build is completed, the parts are removed from stock and the BuildItemAllocation objects are removed.
|
||||
|
||||
Attributes:
|
||||
build: Link to a Build object
|
||||
@ -1175,33 +1075,28 @@ class BuildItem(models.Model):
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
"""Return the API URL used to access this model"""
|
||||
return reverse('api-build-item-list')
|
||||
|
||||
def get_absolute_url(self):
|
||||
# TODO - Fix!
|
||||
return '/build/item/{pk}/'.format(pk=self.id)
|
||||
# return reverse('build-detail', kwargs={'pk': self.id})
|
||||
|
||||
class Meta:
|
||||
"""Serializer metaclass"""
|
||||
unique_together = [
|
||||
('build', 'stock_item', 'install_into'),
|
||||
]
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
"""Custom save method for the BuildItem model"""
|
||||
self.clean()
|
||||
|
||||
super().save()
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Check validity of this BuildItem instance.
|
||||
The following checks are performed:
|
||||
"""Check validity of this BuildItem instance.
|
||||
|
||||
The following checks are performed:
|
||||
- StockItem.part must be in the BOM of the Part object referenced by Build
|
||||
- Allocation quantity cannot exceed available quantity
|
||||
"""
|
||||
|
||||
self.validate_unique()
|
||||
|
||||
super().clean()
|
||||
@ -1303,13 +1198,11 @@ class BuildItem(models.Model):
|
||||
|
||||
@transaction.atomic
|
||||
def complete_allocation(self, user, notes=''):
|
||||
"""
|
||||
Complete the allocation of this BuildItem into the output stock item.
|
||||
"""Complete the allocation of this BuildItem into the output stock item.
|
||||
|
||||
- If the referenced part is trackable, the stock item will be *installed* into the build output
|
||||
- If the referenced part is *not* trackable, the stock item will be removed from stock
|
||||
"""
|
||||
|
||||
item = self.stock_item
|
||||
|
||||
# For a trackable part, special consideration needed!
|
||||
@ -1344,10 +1237,7 @@ class BuildItem(models.Model):
|
||||
)
|
||||
|
||||
def getStockItemThumbnail(self):
|
||||
"""
|
||||
Return qualified URL for part thumbnail image
|
||||
"""
|
||||
|
||||
"""Return qualified URL for part thumbnail image."""
|
||||
thumb_url = None
|
||||
|
||||
if self.stock_item and self.stock_item.part:
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
JSON serializers for Build API
|
||||
"""
|
||||
"""JSON serializers for Build API."""
|
||||
|
||||
from django.db import transaction
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
@ -31,9 +29,7 @@ from .models import Build, BuildItem, BuildOrderAttachment
|
||||
|
||||
|
||||
class BuildSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
|
||||
"""
|
||||
Serializes a Build object
|
||||
"""
|
||||
"""Serializes a Build object."""
|
||||
|
||||
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
||||
status_text = serializers.CharField(source='get_status_display', read_only=True)
|
||||
@ -50,16 +46,12 @@ class BuildSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer
|
||||
|
||||
@staticmethod
|
||||
def annotate_queryset(queryset):
|
||||
"""
|
||||
Add custom annotations to the BuildSerializer queryset,
|
||||
performing database queries as efficiently as possible.
|
||||
"""Add custom annotations to the BuildSerializer queryset, performing database queries as efficiently as possible.
|
||||
|
||||
The following annoted fields are added:
|
||||
|
||||
- overdue: True if the build is outstanding *and* the completion date has past
|
||||
|
||||
"""
|
||||
|
||||
# Annotate a boolean 'overdue' flag
|
||||
|
||||
queryset = queryset.annotate(
|
||||
@ -74,6 +66,7 @@ class BuildSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer
|
||||
return queryset
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Determine if extra serializer fields are required"""
|
||||
part_detail = kwargs.pop('part_detail', True)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -82,6 +75,7 @@ class BuildSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer
|
||||
self.fields.pop('part_detail')
|
||||
|
||||
class Meta:
|
||||
"""Serializer metaclass"""
|
||||
model = Build
|
||||
fields = [
|
||||
'pk',
|
||||
@ -121,8 +115,7 @@ class BuildSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer
|
||||
|
||||
|
||||
class BuildOutputSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for a "BuildOutput"
|
||||
"""Serializer for a "BuildOutput".
|
||||
|
||||
Note that a "BuildOutput" is really just a StockItem which is "in production"!
|
||||
"""
|
||||
@ -136,7 +129,7 @@ class BuildOutputSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate_output(self, output):
|
||||
|
||||
"""Perform validation for the output (StockItem) provided to the serializer"""
|
||||
build = self.context['build']
|
||||
|
||||
# As this serializer can be used in multiple contexts, we need to work out why we are here
|
||||
@ -168,14 +161,14 @@ class BuildOutputSerializer(serializers.Serializer):
|
||||
return output
|
||||
|
||||
class Meta:
|
||||
"""Serializer metaclass"""
|
||||
fields = [
|
||||
'output',
|
||||
]
|
||||
|
||||
|
||||
class BuildOutputCreateSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for creating a new BuildOutput against a BuildOrder.
|
||||
"""Serializer for creating a new BuildOutput against a BuildOrder.
|
||||
|
||||
URL pattern is "/api/build/<pk>/create-output/", where <pk> is the PK of a Build.
|
||||
|
||||
@ -192,13 +185,15 @@ class BuildOutputCreateSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def get_build(self):
|
||||
"""Return the Build instance associated with this serializer"""
|
||||
return self.context["build"]
|
||||
|
||||
def get_part(self):
|
||||
"""Return the Part instance associated with the build"""
|
||||
return self.get_build().part
|
||||
|
||||
def validate_quantity(self, quantity):
|
||||
|
||||
"""Validate the provided quantity field"""
|
||||
if quantity <= 0:
|
||||
raise ValidationError(_("Quantity must be greater than zero"))
|
||||
|
||||
@ -229,7 +224,7 @@ class BuildOutputCreateSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate_serial_numbers(self, serial_numbers):
|
||||
|
||||
"""Clean the provided serial number string"""
|
||||
serial_numbers = serial_numbers.strip()
|
||||
|
||||
return serial_numbers
|
||||
@ -243,10 +238,7 @@ class BuildOutputCreateSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate(self, data):
|
||||
"""
|
||||
Perform form validation
|
||||
"""
|
||||
|
||||
"""Perform form validation."""
|
||||
part = self.get_part()
|
||||
|
||||
# Cache a list of serial numbers (to be used in the "save" method)
|
||||
@ -284,10 +276,7 @@ class BuildOutputCreateSerializer(serializers.Serializer):
|
||||
return data
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Generate the new build output(s)
|
||||
"""
|
||||
|
||||
"""Generate the new build output(s)"""
|
||||
data = self.validated_data
|
||||
|
||||
quantity = data['quantity']
|
||||
@ -305,11 +294,10 @@ class BuildOutputCreateSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class BuildOutputDeleteSerializer(serializers.Serializer):
|
||||
"""
|
||||
DRF serializer for deleting (cancelling) one or more build outputs
|
||||
"""
|
||||
"""DRF serializer for deleting (cancelling) one or more build outputs."""
|
||||
|
||||
class Meta:
|
||||
"""Serializer metaclass"""
|
||||
fields = [
|
||||
'outputs',
|
||||
]
|
||||
@ -320,7 +308,7 @@ class BuildOutputDeleteSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate(self, data):
|
||||
|
||||
"""Perform data validation for this serializer"""
|
||||
data = super().validate(data)
|
||||
|
||||
outputs = data.get('outputs', [])
|
||||
@ -331,10 +319,7 @@ class BuildOutputDeleteSerializer(serializers.Serializer):
|
||||
return data
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
'save' the serializer to delete the build outputs
|
||||
"""
|
||||
|
||||
"""'save' the serializer to delete the build outputs."""
|
||||
data = self.validated_data
|
||||
outputs = data.get('outputs', [])
|
||||
|
||||
@ -347,11 +332,10 @@ class BuildOutputDeleteSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class BuildOutputCompleteSerializer(serializers.Serializer):
|
||||
"""
|
||||
DRF serializer for completing one or more build outputs
|
||||
"""
|
||||
"""DRF serializer for completing one or more build outputs."""
|
||||
|
||||
class Meta:
|
||||
"""Serializer metaclass"""
|
||||
fields = [
|
||||
'outputs',
|
||||
'location',
|
||||
@ -393,7 +377,7 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate(self, data):
|
||||
|
||||
"""Perform data validation for this serializer"""
|
||||
super().validate(data)
|
||||
|
||||
outputs = data.get('outputs', [])
|
||||
@ -404,10 +388,7 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
|
||||
return data
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
"save" the serializer to complete the build outputs
|
||||
"""
|
||||
|
||||
"""Save the serializer to complete the build outputs."""
|
||||
build = self.context['build']
|
||||
request = self.context['request']
|
||||
|
||||
@ -435,15 +416,17 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class BuildCancelSerializer(serializers.Serializer):
|
||||
"""DRF serializer class for cancelling an active BuildOrder"""
|
||||
|
||||
class Meta:
|
||||
"""Serializer metaclass"""
|
||||
fields = [
|
||||
'remove_allocated_stock',
|
||||
'remove_incomplete_outputs',
|
||||
]
|
||||
|
||||
def get_context_data(self):
|
||||
|
||||
"""Retrieve extra context data from this serializer"""
|
||||
build = self.context['build']
|
||||
|
||||
return {
|
||||
@ -467,7 +450,7 @@ class BuildCancelSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def save(self):
|
||||
|
||||
"""Cancel the specified build"""
|
||||
build = self.context['build']
|
||||
request = self.context['request']
|
||||
|
||||
@ -481,9 +464,7 @@ class BuildCancelSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class BuildCompleteSerializer(serializers.Serializer):
|
||||
"""
|
||||
DRF serializer for marking a BuildOrder as complete
|
||||
"""
|
||||
"""DRF serializer for marking a BuildOrder as complete."""
|
||||
|
||||
accept_unallocated = serializers.BooleanField(
|
||||
label=_('Accept Unallocated'),
|
||||
@ -493,7 +474,7 @@ class BuildCompleteSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate_accept_unallocated(self, value):
|
||||
|
||||
"""Check if the 'accept_unallocated' field is required"""
|
||||
build = self.context['build']
|
||||
|
||||
if not build.are_untracked_parts_allocated() and not value:
|
||||
@ -509,7 +490,7 @@ class BuildCompleteSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate_accept_incomplete(self, value):
|
||||
|
||||
"""Check if the 'accept_incomplete' field is required"""
|
||||
build = self.context['build']
|
||||
|
||||
if build.remaining > 0 and not value:
|
||||
@ -518,7 +499,7 @@ class BuildCompleteSerializer(serializers.Serializer):
|
||||
return value
|
||||
|
||||
def validate(self, data):
|
||||
|
||||
"""Perform validation of this serializer prior to saving"""
|
||||
build = self.context['build']
|
||||
|
||||
if build.incomplete_count > 0:
|
||||
@ -530,7 +511,7 @@ class BuildCompleteSerializer(serializers.Serializer):
|
||||
return data
|
||||
|
||||
def save(self):
|
||||
|
||||
"""Complete the specified build output"""
|
||||
request = self.context['request']
|
||||
build = self.context['build']
|
||||
|
||||
@ -538,14 +519,12 @@ class BuildCompleteSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class BuildUnallocationSerializer(serializers.Serializer):
|
||||
"""
|
||||
DRF serializer for unallocating stock from a BuildOrder
|
||||
"""DRF serializer for unallocating stock from a BuildOrder.
|
||||
|
||||
Allocated stock can be unallocated with a number of filters:
|
||||
|
||||
- output: Filter against a particular build output (blank = untracked stock)
|
||||
- bom_item: Filter against a particular BOM line item
|
||||
|
||||
"""
|
||||
|
||||
bom_item = serializers.PrimaryKeyRelatedField(
|
||||
@ -567,8 +546,7 @@ class BuildUnallocationSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate_output(self, stock_item):
|
||||
|
||||
# Stock item must point to the same build order!
|
||||
"""Validation for the output StockItem instance. Stock item must point to the same build order!"""
|
||||
build = self.context['build']
|
||||
|
||||
if stock_item and stock_item.build != build:
|
||||
@ -577,11 +555,10 @@ class BuildUnallocationSerializer(serializers.Serializer):
|
||||
return stock_item
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
'Save' the serializer data.
|
||||
"""Save the serializer data.
|
||||
|
||||
This performs the actual unallocation against the build order
|
||||
"""
|
||||
|
||||
build = self.context['build']
|
||||
|
||||
data = self.validated_data
|
||||
@ -593,9 +570,7 @@ class BuildUnallocationSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class BuildAllocationItemSerializer(serializers.Serializer):
|
||||
"""
|
||||
A serializer for allocating a single stock item against a build order
|
||||
"""
|
||||
"""A serializer for allocating a single stock item against a build order."""
|
||||
|
||||
bom_item = serializers.PrimaryKeyRelatedField(
|
||||
queryset=BomItem.objects.all(),
|
||||
@ -606,10 +581,7 @@ class BuildAllocationItemSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate_bom_item(self, bom_item):
|
||||
"""
|
||||
Check if the parts match!
|
||||
"""
|
||||
|
||||
"""Check if the parts match"""
|
||||
build = self.context['build']
|
||||
|
||||
# BomItem should point to the same 'part' as the parent build
|
||||
@ -632,7 +604,7 @@ class BuildAllocationItemSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate_stock_item(self, stock_item):
|
||||
|
||||
"""Perform validation of the stock_item field"""
|
||||
if not stock_item.in_stock:
|
||||
raise ValidationError(_("Item must be in stock"))
|
||||
|
||||
@ -646,7 +618,7 @@ class BuildAllocationItemSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def validate_quantity(self, quantity):
|
||||
|
||||
"""Perform validation of the 'quantity' field"""
|
||||
if quantity <= 0:
|
||||
raise ValidationError(_("Quantity must be greater than zero"))
|
||||
|
||||
@ -661,6 +633,7 @@ class BuildAllocationItemSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
"""Serializer metaclass"""
|
||||
fields = [
|
||||
'bom_item',
|
||||
'stock_item',
|
||||
@ -669,7 +642,7 @@ class BuildAllocationItemSerializer(serializers.Serializer):
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
|
||||
"""Perfofrm data validation for this item"""
|
||||
super().validate(data)
|
||||
|
||||
build = self.context['build']
|
||||
@ -715,22 +688,18 @@ class BuildAllocationItemSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class BuildAllocationSerializer(serializers.Serializer):
|
||||
"""
|
||||
DRF serializer for allocation stock items against a build order
|
||||
"""
|
||||
"""DRF serializer for allocation stock items against a build order."""
|
||||
|
||||
items = BuildAllocationItemSerializer(many=True)
|
||||
|
||||
class Meta:
|
||||
"""Serializer metaclass"""
|
||||
fields = [
|
||||
'items',
|
||||
]
|
||||
|
||||
def validate(self, data):
|
||||
"""
|
||||
Validation
|
||||
"""
|
||||
|
||||
"""Validation."""
|
||||
data = super().validate(data)
|
||||
|
||||
items = data.get('items', [])
|
||||
@ -741,7 +710,7 @@ class BuildAllocationSerializer(serializers.Serializer):
|
||||
return data
|
||||
|
||||
def save(self):
|
||||
|
||||
"""Perform the allocation"""
|
||||
data = self.validated_data
|
||||
|
||||
items = data.get('items', [])
|
||||
@ -770,11 +739,10 @@ class BuildAllocationSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class BuildAutoAllocationSerializer(serializers.Serializer):
|
||||
"""
|
||||
DRF serializer for auto allocating stock items against a build order
|
||||
"""
|
||||
"""DRF serializer for auto allocating stock items against a build order."""
|
||||
|
||||
class Meta:
|
||||
"""Serializer metaclass"""
|
||||
fields = [
|
||||
'location',
|
||||
'exclude_location',
|
||||
@ -813,7 +781,7 @@ class BuildAutoAllocationSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
def save(self):
|
||||
|
||||
"""Perform the auto-allocation step"""
|
||||
data = self.validated_data
|
||||
|
||||
build = self.context['build']
|
||||
@ -827,7 +795,7 @@ class BuildAutoAllocationSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class BuildItemSerializer(InvenTreeModelSerializer):
|
||||
""" Serializes a BuildItem object """
|
||||
"""Serializes a BuildItem object."""
|
||||
|
||||
bom_part = serializers.IntegerField(source='bom_item.sub_part.pk', read_only=True)
|
||||
part = serializers.IntegerField(source='stock_item.part.pk', read_only=True)
|
||||
@ -842,7 +810,7 @@ class BuildItemSerializer(InvenTreeModelSerializer):
|
||||
quantity = InvenTreeDecimalField()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
"""Determine which extra details fields should be included"""
|
||||
build_detail = kwargs.pop('build_detail', False)
|
||||
part_detail = kwargs.pop('part_detail', False)
|
||||
location_detail = kwargs.pop('location_detail', False)
|
||||
@ -859,6 +827,7 @@ class BuildItemSerializer(InvenTreeModelSerializer):
|
||||
self.fields.pop('location_detail')
|
||||
|
||||
class Meta:
|
||||
"""Serializer metaclass"""
|
||||
model = BuildItem
|
||||
fields = [
|
||||
'pk',
|
||||
@ -877,11 +846,10 @@ class BuildItemSerializer(InvenTreeModelSerializer):
|
||||
|
||||
|
||||
class BuildAttachmentSerializer(InvenTreeAttachmentSerializer):
|
||||
"""
|
||||
Serializer for a BuildAttachment
|
||||
"""
|
||||
"""Serializer for a BuildAttachment."""
|
||||
|
||||
class Meta:
|
||||
"""Serializer metaclass"""
|
||||
model = BuildOrderAttachment
|
||||
|
||||
fields = [
|
||||
|
@ -1,3 +1,5 @@
|
||||
"""Background task definitions for the BuildOrder app"""
|
||||
|
||||
from decimal import Decimal
|
||||
import logging
|
||||
|
||||
@ -18,11 +20,10 @@ logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
def check_build_stock(build: build.models.Build):
|
||||
"""
|
||||
Check the required stock for a newly created build order,
|
||||
and send an email out to any subscribed users if stock is low.
|
||||
"""
|
||||
"""Check the required stock for a newly created build order.
|
||||
|
||||
Send an email out to any subscribed users if stock is low.
|
||||
"""
|
||||
# Do not notify if we are importing data
|
||||
if isImportingData():
|
||||
return
|
||||
|
@ -1,3 +1,5 @@
|
||||
"""Unit tests for the BuildOrder API"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.urls import reverse
|
||||
@ -13,8 +15,8 @@ from InvenTree.api_tester import InvenTreeAPITestCase
|
||||
|
||||
|
||||
class TestBuildAPI(InvenTreeAPITestCase):
|
||||
"""
|
||||
Series of tests for the Build DRF API
|
||||
"""Series of tests for the Build DRF API.
|
||||
|
||||
- Tests for Build API
|
||||
- Tests for BuildItem API
|
||||
"""
|
||||
@ -33,10 +35,7 @@ class TestBuildAPI(InvenTreeAPITestCase):
|
||||
]
|
||||
|
||||
def test_get_build_list(self):
|
||||
"""
|
||||
Test that we can retrieve list of build objects
|
||||
"""
|
||||
|
||||
"""Test that we can retrieve list of build objects."""
|
||||
url = reverse('api-build-list')
|
||||
response = self.client.get(url, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@ -65,7 +64,7 @@ class TestBuildAPI(InvenTreeAPITestCase):
|
||||
self.assertEqual(len(response.data), 0)
|
||||
|
||||
def test_get_build_item_list(self):
|
||||
""" Test that we can retrieve list of BuildItem objects """
|
||||
"""Test that we can retrieve list of BuildItem objects."""
|
||||
url = reverse('api-build-item-list')
|
||||
|
||||
response = self.client.get(url, format='json')
|
||||
@ -77,9 +76,7 @@ class TestBuildAPI(InvenTreeAPITestCase):
|
||||
|
||||
|
||||
class BuildAPITest(InvenTreeAPITestCase):
|
||||
"""
|
||||
Series of tests for the Build DRF API
|
||||
"""
|
||||
"""Series of tests for the Build DRF API."""
|
||||
|
||||
fixtures = [
|
||||
'category',
|
||||
@ -96,18 +93,12 @@ class BuildAPITest(InvenTreeAPITestCase):
|
||||
'build.add'
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super().setUp()
|
||||
|
||||
|
||||
class BuildTest(BuildAPITest):
|
||||
"""
|
||||
Unit testing for the build complete API endpoint
|
||||
"""
|
||||
"""Unit testing for the build complete API endpoint."""
|
||||
|
||||
def setUp(self):
|
||||
|
||||
"""Basic setup for this test suite"""
|
||||
super().setUp()
|
||||
|
||||
self.build = Build.objects.get(pk=1)
|
||||
@ -115,10 +106,7 @@ class BuildTest(BuildAPITest):
|
||||
self.url = reverse('api-build-output-complete', kwargs={'pk': self.build.pk})
|
||||
|
||||
def test_invalid(self):
|
||||
"""
|
||||
Test with invalid data
|
||||
"""
|
||||
|
||||
"""Test with invalid data."""
|
||||
# Test with an invalid build ID
|
||||
self.post(
|
||||
reverse('api-build-output-complete', kwargs={'pk': 99999}),
|
||||
@ -199,10 +187,7 @@ class BuildTest(BuildAPITest):
|
||||
)
|
||||
|
||||
def test_complete(self):
|
||||
"""
|
||||
Test build order completion
|
||||
"""
|
||||
|
||||
"""Test build order completion."""
|
||||
# Initially, build should not be able to be completed
|
||||
self.assertFalse(self.build.can_complete)
|
||||
|
||||
@ -270,8 +255,7 @@ class BuildTest(BuildAPITest):
|
||||
self.assertTrue(self.build.is_complete)
|
||||
|
||||
def test_cancel(self):
|
||||
""" Test that we can cancel a BuildOrder via the API """
|
||||
|
||||
"""Test that we can cancel a BuildOrder via the API."""
|
||||
bo = Build.objects.get(pk=1)
|
||||
|
||||
url = reverse('api-build-cancel', kwargs={'pk': bo.pk})
|
||||
@ -285,10 +269,7 @@ class BuildTest(BuildAPITest):
|
||||
self.assertEqual(bo.status, BuildStatus.CANCELLED)
|
||||
|
||||
def test_create_delete_output(self):
|
||||
"""
|
||||
Test that we can create and delete build outputs via the API
|
||||
"""
|
||||
|
||||
"""Test that we can create and delete build outputs via the API."""
|
||||
bo = Build.objects.get(pk=1)
|
||||
|
||||
n_outputs = bo.output_count
|
||||
@ -494,7 +475,7 @@ class BuildTest(BuildAPITest):
|
||||
self.assertIn('This build output has already been completed', str(response.data))
|
||||
|
||||
def test_download_build_orders(self):
|
||||
|
||||
"""Test that we can download a list of build orders via the API"""
|
||||
required_cols = [
|
||||
'reference',
|
||||
'status',
|
||||
@ -539,19 +520,17 @@ class BuildTest(BuildAPITest):
|
||||
|
||||
|
||||
class BuildAllocationTest(BuildAPITest):
|
||||
"""
|
||||
Unit tests for allocation of stock items against a build order.
|
||||
"""Unit tests for allocation of stock items against a build order.
|
||||
|
||||
For this test, we will be using Build ID=1;
|
||||
|
||||
- This points to Part 100 (see fixture data in part.yaml)
|
||||
- This Part already has a BOM with 4 items (see fixture data in bom.yaml)
|
||||
- There are no BomItem objects yet created for this build
|
||||
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
|
||||
"""Basic operation as part of test suite setup"""
|
||||
super().setUp()
|
||||
|
||||
self.assignRole('build.add')
|
||||
@ -565,10 +544,7 @@ class BuildAllocationTest(BuildAPITest):
|
||||
self.n = BuildItem.objects.count()
|
||||
|
||||
def test_build_data(self):
|
||||
"""
|
||||
Check that our assumptions about the particular BuildOrder are correct
|
||||
"""
|
||||
|
||||
"""Check that our assumptions about the particular BuildOrder are correct."""
|
||||
self.assertEqual(self.build.part.pk, 100)
|
||||
|
||||
# There should be 4x BOM items we can use
|
||||
@ -578,26 +554,17 @@ class BuildAllocationTest(BuildAPITest):
|
||||
self.assertEqual(self.build.allocated_stock.count(), 0)
|
||||
|
||||
def test_get(self):
|
||||
"""
|
||||
A GET request to the endpoint should return an error
|
||||
"""
|
||||
|
||||
"""A GET request to the endpoint should return an error."""
|
||||
self.get(self.url, expected_code=405)
|
||||
|
||||
def test_options(self):
|
||||
"""
|
||||
An OPTIONS request to the endpoint should return information about the endpoint
|
||||
"""
|
||||
|
||||
"""An OPTIONS request to the endpoint should return information about the endpoint."""
|
||||
response = self.options(self.url, expected_code=200)
|
||||
|
||||
self.assertIn("API endpoint to allocate stock items to a build order", str(response.data))
|
||||
|
||||
def test_empty(self):
|
||||
"""
|
||||
Test without any POST data
|
||||
"""
|
||||
|
||||
"""Test without any POST data."""
|
||||
# Initially test with an empty data set
|
||||
data = self.post(self.url, {}, expected_code=400).data
|
||||
|
||||
@ -618,10 +585,7 @@ class BuildAllocationTest(BuildAPITest):
|
||||
self.assertEqual(self.n, BuildItem.objects.count())
|
||||
|
||||
def test_missing(self):
|
||||
"""
|
||||
Test with missing data
|
||||
"""
|
||||
|
||||
"""Test with missing data."""
|
||||
# Missing quantity
|
||||
data = self.post(
|
||||
self.url,
|
||||
@ -674,10 +638,7 @@ class BuildAllocationTest(BuildAPITest):
|
||||
self.assertEqual(self.n, BuildItem.objects.count())
|
||||
|
||||
def test_invalid_bom_item(self):
|
||||
"""
|
||||
Test by passing an invalid BOM item
|
||||
"""
|
||||
|
||||
"""Test by passing an invalid BOM item."""
|
||||
data = self.post(
|
||||
self.url,
|
||||
{
|
||||
@ -695,11 +656,10 @@ class BuildAllocationTest(BuildAPITest):
|
||||
self.assertIn('must point to the same part', str(data))
|
||||
|
||||
def test_valid_data(self):
|
||||
"""
|
||||
Test with valid data.
|
||||
"""Test with valid data.
|
||||
|
||||
This should result in creation of a new BuildItem object
|
||||
"""
|
||||
|
||||
self.post(
|
||||
self.url,
|
||||
{
|
||||
@ -725,17 +685,12 @@ class BuildAllocationTest(BuildAPITest):
|
||||
|
||||
|
||||
class BuildListTest(BuildAPITest):
|
||||
"""
|
||||
Tests for the BuildOrder LIST API
|
||||
"""
|
||||
"""Tests for the BuildOrder LIST API."""
|
||||
|
||||
url = reverse('api-build-list')
|
||||
|
||||
def test_get_all_builds(self):
|
||||
"""
|
||||
Retrieve *all* builds via the API
|
||||
"""
|
||||
|
||||
"""Retrieve *all* builds via the API."""
|
||||
builds = self.get(self.url)
|
||||
|
||||
self.assertEqual(len(builds.data), 5)
|
||||
@ -753,10 +708,7 @@ class BuildListTest(BuildAPITest):
|
||||
self.assertEqual(len(builds.data), 0)
|
||||
|
||||
def test_overdue(self):
|
||||
"""
|
||||
Create a new build, in the past
|
||||
"""
|
||||
|
||||
"""Create a new build, in the past."""
|
||||
in_the_past = datetime.now().date() - timedelta(days=50)
|
||||
|
||||
part = Part.objects.get(pk=50)
|
||||
@ -776,10 +728,7 @@ class BuildListTest(BuildAPITest):
|
||||
self.assertEqual(len(builds), 1)
|
||||
|
||||
def test_sub_builds(self):
|
||||
"""
|
||||
Test the build / sub-build relationship
|
||||
"""
|
||||
|
||||
"""Test the build / sub-build relationship."""
|
||||
parent = Build.objects.get(pk=5)
|
||||
|
||||
part = Part.objects.get(pk=50)
|
||||
|
@ -1,4 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Unit tests for the 'build' models"""
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
@ -12,13 +12,10 @@ from stock.models import StockItem
|
||||
|
||||
|
||||
class BuildTestBase(TestCase):
|
||||
"""
|
||||
Run some tests to ensure that the Build model is working properly.
|
||||
"""
|
||||
"""Run some tests to ensure that the Build model is working properly."""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Initialize data to use for these tests.
|
||||
"""Initialize data to use for these tests.
|
||||
|
||||
The base Part 'assembly' has a BOM consisting of three parts:
|
||||
|
||||
@ -119,11 +116,10 @@ class BuildTestBase(TestCase):
|
||||
|
||||
|
||||
class BuildTest(BuildTestBase):
|
||||
"""Unit testing class for the Build model"""
|
||||
|
||||
def test_ref_int(self):
|
||||
"""
|
||||
Test the "integer reference" field used for natural sorting
|
||||
"""
|
||||
"""Test the "integer reference" field used for natural sorting"""
|
||||
|
||||
for ii in range(10):
|
||||
build = Build(
|
||||
@ -141,7 +137,7 @@ class BuildTest(BuildTestBase):
|
||||
self.assertEqual(build.reference_int, ii)
|
||||
|
||||
def test_init(self):
|
||||
# Perform some basic tests before we start the ball rolling
|
||||
"""Perform some basic tests before we start the ball rolling"""
|
||||
|
||||
self.assertEqual(StockItem.objects.count(), 10)
|
||||
|
||||
@ -166,7 +162,7 @@ class BuildTest(BuildTestBase):
|
||||
self.assertFalse(self.build.is_complete)
|
||||
|
||||
def test_build_item_clean(self):
|
||||
# Ensure that dodgy BuildItem objects cannot be created
|
||||
"""Ensure that dodgy BuildItem objects cannot be created"""
|
||||
|
||||
stock = StockItem.objects.create(part=self.assembly, quantity=99)
|
||||
|
||||
@ -193,7 +189,7 @@ class BuildTest(BuildTestBase):
|
||||
b.save()
|
||||
|
||||
def test_duplicate_bom_line(self):
|
||||
# Try to add a duplicate BOM item - it should be allowed
|
||||
"""Try to add a duplicate BOM item - it should be allowed"""
|
||||
|
||||
BomItem.objects.create(
|
||||
part=self.assembly,
|
||||
@ -202,12 +198,11 @@ class BuildTest(BuildTestBase):
|
||||
)
|
||||
|
||||
def allocate_stock(self, output, allocations):
|
||||
"""
|
||||
Allocate stock to this build, against a particular output
|
||||
"""Allocate stock to this build, against a particular output
|
||||
|
||||
Args:
|
||||
output - StockItem object (or None)
|
||||
allocations - Map of {StockItem: quantity}
|
||||
output: StockItem object (or None)
|
||||
allocations: Map of {StockItem: quantity}
|
||||
"""
|
||||
|
||||
for item, quantity in allocations.items():
|
||||
@ -219,9 +214,7 @@ class BuildTest(BuildTestBase):
|
||||
)
|
||||
|
||||
def test_partial_allocation(self):
|
||||
"""
|
||||
Test partial allocation of stock
|
||||
"""
|
||||
"""Test partial allocation of stock"""
|
||||
|
||||
# Fully allocate tracked stock against build output 1
|
||||
self.allocate_stock(
|
||||
@ -294,9 +287,7 @@ class BuildTest(BuildTestBase):
|
||||
self.assertTrue(self.build.are_untracked_parts_allocated())
|
||||
|
||||
def test_cancel(self):
|
||||
"""
|
||||
Test cancellation of the build
|
||||
"""
|
||||
"""Test cancellation of the build"""
|
||||
|
||||
# TODO
|
||||
|
||||
@ -309,9 +300,7 @@ class BuildTest(BuildTestBase):
|
||||
pass
|
||||
|
||||
def test_complete(self):
|
||||
"""
|
||||
Test completion of a build output
|
||||
"""
|
||||
"""Test completion of a build output"""
|
||||
|
||||
self.stock_1_1.quantity = 1000
|
||||
self.stock_1_1.save()
|
||||
@ -385,12 +374,10 @@ class BuildTest(BuildTestBase):
|
||||
|
||||
|
||||
class AutoAllocationTests(BuildTestBase):
|
||||
"""
|
||||
Tests for auto allocating stock against a build order
|
||||
"""
|
||||
"""Tests for auto allocating stock against a build order"""
|
||||
|
||||
def setUp(self):
|
||||
|
||||
"""Init routines for this unit test class"""
|
||||
super().setUp()
|
||||
|
||||
# Add a "substitute" part for bom_item_2
|
||||
@ -411,8 +398,7 @@ class AutoAllocationTests(BuildTestBase):
|
||||
)
|
||||
|
||||
def test_auto_allocate(self):
|
||||
"""
|
||||
Run the 'auto-allocate' function. What do we expect to happen?
|
||||
"""Run the 'auto-allocate' function. What do we expect to happen?
|
||||
|
||||
There are two "untracked" parts:
|
||||
- sub_part_1 (quantity 5 per BOM = 50 required total) / 103 in stock (2 items)
|
||||
@ -474,9 +460,7 @@ class AutoAllocationTests(BuildTestBase):
|
||||
self.assertTrue(self.build.is_bom_item_allocated(self.bom_item_2))
|
||||
|
||||
def test_fully_auto(self):
|
||||
"""
|
||||
We should be able to auto-allocate against a build in a single go
|
||||
"""
|
||||
"""We should be able to auto-allocate against a build in a single go"""
|
||||
|
||||
self.build.auto_allocate_stock(
|
||||
interchangeable=True,
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
Tests for the build model database migrations
|
||||
"""
|
||||
"""Tests for the build model database migrations."""
|
||||
|
||||
from django_test_migrations.contrib.unittest_case import MigratorTestCase
|
||||
|
||||
@ -8,18 +6,13 @@ from InvenTree import helpers
|
||||
|
||||
|
||||
class TestForwardMigrations(MigratorTestCase):
|
||||
"""
|
||||
Test entire schema migration sequence for the build app
|
||||
"""
|
||||
"""Test entire schema migration sequence for the build app."""
|
||||
|
||||
migrate_from = ('build', helpers.getOldestMigrationFile('build'))
|
||||
migrate_to = ('build', helpers.getNewestMigrationFile('build'))
|
||||
|
||||
def prepare(self):
|
||||
"""
|
||||
Create initial data!
|
||||
"""
|
||||
|
||||
"""Create initial data!"""
|
||||
Part = self.old_state.apps.get_model('part', 'part')
|
||||
|
||||
buildable_part = Part.objects.create(
|
||||
@ -45,7 +38,7 @@ class TestForwardMigrations(MigratorTestCase):
|
||||
)
|
||||
|
||||
def test_items_exist(self):
|
||||
|
||||
"""Test to ensure that the 'assembly' field is correctly configured"""
|
||||
Part = self.new_state.apps.get_model('part', 'part')
|
||||
|
||||
self.assertEqual(Part.objects.count(), 1)
|
||||
@ -63,18 +56,13 @@ class TestForwardMigrations(MigratorTestCase):
|
||||
|
||||
|
||||
class TestReferenceMigration(MigratorTestCase):
|
||||
"""
|
||||
Test custom migration which adds 'reference' field to Build model
|
||||
"""
|
||||
"""Test custom migration which adds 'reference' field to Build model."""
|
||||
|
||||
migrate_from = ('build', helpers.getOldestMigrationFile('build'))
|
||||
migrate_to = ('build', '0018_build_reference')
|
||||
|
||||
def prepare(self):
|
||||
"""
|
||||
Create some builds
|
||||
"""
|
||||
|
||||
"""Create some builds."""
|
||||
Part = self.old_state.apps.get_model('part', 'part')
|
||||
|
||||
part = Part.objects.create(
|
||||
@ -108,7 +96,7 @@ class TestReferenceMigration(MigratorTestCase):
|
||||
print(build.reference)
|
||||
|
||||
def test_build_reference(self):
|
||||
|
||||
"""Test that the build reference is correctly assigned to the PK of the Build"""
|
||||
Build = self.new_state.apps.get_model('build', 'build')
|
||||
|
||||
self.assertEqual(Build.objects.count(), 3)
|
||||
|
@ -1,3 +1,5 @@
|
||||
"""Basic unit tests for the BuildOrder app"""
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
@ -11,6 +13,7 @@ from InvenTree.status_codes import BuildStatus
|
||||
|
||||
|
||||
class BuildTestSimple(InvenTreeTestCase):
|
||||
"""Basic set of tests for the BuildOrder model functionality"""
|
||||
|
||||
fixtures = [
|
||||
'category',
|
||||
@ -26,7 +29,7 @@ class BuildTestSimple(InvenTreeTestCase):
|
||||
]
|
||||
|
||||
def test_build_objects(self):
|
||||
# Ensure the Build objects were correctly created
|
||||
"""Ensure the Build objects were correctly created"""
|
||||
self.assertEqual(Build.objects.count(), 5)
|
||||
b = Build.objects.get(pk=2)
|
||||
self.assertEqual(b.batch, 'B2')
|
||||
@ -35,10 +38,12 @@ class BuildTestSimple(InvenTreeTestCase):
|
||||
self.assertEqual(str(b), 'BO0002')
|
||||
|
||||
def test_url(self):
|
||||
"""Test URL lookup"""
|
||||
b1 = Build.objects.get(pk=1)
|
||||
self.assertEqual(b1.get_absolute_url(), '/build/1/')
|
||||
|
||||
def test_is_complete(self):
|
||||
"""Test build completion status"""
|
||||
b1 = Build.objects.get(pk=1)
|
||||
b2 = Build.objects.get(pk=2)
|
||||
|
||||
@ -48,10 +53,7 @@ class BuildTestSimple(InvenTreeTestCase):
|
||||
self.assertEqual(b2.status, BuildStatus.COMPLETE)
|
||||
|
||||
def test_overdue(self):
|
||||
"""
|
||||
Test overdue status functionality
|
||||
"""
|
||||
|
||||
"""Test overdue status functionality."""
|
||||
today = datetime.now().date()
|
||||
|
||||
build = Build.objects.get(pk=1)
|
||||
@ -66,6 +68,7 @@ class BuildTestSimple(InvenTreeTestCase):
|
||||
self.assertFalse(build.is_overdue)
|
||||
|
||||
def test_is_active(self):
|
||||
"""Test active / inactive build status"""
|
||||
b1 = Build.objects.get(pk=1)
|
||||
b2 = Build.objects.get(pk=2)
|
||||
|
||||
@ -73,12 +76,12 @@ class BuildTestSimple(InvenTreeTestCase):
|
||||
self.assertEqual(b2.is_active, False)
|
||||
|
||||
def test_required_parts(self):
|
||||
# TODO - Generate BOM for test part
|
||||
pass
|
||||
"""Test set of required BOM items for the build"""
|
||||
# TODO: Generate BOM for test part
|
||||
...
|
||||
|
||||
def test_cancel_build(self):
|
||||
""" Test build cancellation function """
|
||||
|
||||
"""Test build cancellation function."""
|
||||
build = Build.objects.get(id=1)
|
||||
|
||||
self.assertEqual(build.status, BuildStatus.PENDING)
|
||||
@ -89,7 +92,7 @@ class BuildTestSimple(InvenTreeTestCase):
|
||||
|
||||
|
||||
class TestBuildViews(InvenTreeTestCase):
|
||||
""" Tests for Build app views """
|
||||
"""Tests for Build app views."""
|
||||
|
||||
fixtures = [
|
||||
'category',
|
||||
@ -105,6 +108,7 @@ class TestBuildViews(InvenTreeTestCase):
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
"""Fixturing for this suite of unit tests"""
|
||||
super().setUp()
|
||||
|
||||
# Create a build output for build # 1
|
||||
@ -118,14 +122,12 @@ class TestBuildViews(InvenTreeTestCase):
|
||||
)
|
||||
|
||||
def test_build_index(self):
|
||||
""" test build index view """
|
||||
|
||||
"""Test build index view."""
|
||||
response = self.client.get(reverse('build-index'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_build_detail(self):
|
||||
""" Test the detail view for a Build object """
|
||||
|
||||
"""Test the detail view for a Build object."""
|
||||
pk = 1
|
||||
|
||||
response = self.client.get(reverse('build-detail', args=(pk,)))
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
URL lookup for Build app
|
||||
"""
|
||||
"""URL lookup for Build app."""
|
||||
|
||||
from django.urls import include, re_path
|
||||
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""
|
||||
Django views for interacting with Build objects
|
||||
"""
|
||||
"""Django views for interacting with Build objects."""
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import DetailView, ListView
|
||||
@ -15,42 +13,25 @@ from plugin.views import InvenTreePluginViewMixin
|
||||
|
||||
|
||||
class BuildIndex(InvenTreeRoleMixin, ListView):
|
||||
"""
|
||||
View for displaying list of Builds
|
||||
"""
|
||||
"""View for displaying list of Builds."""
|
||||
model = Build
|
||||
template_name = 'build/index.html'
|
||||
context_object_name = 'builds'
|
||||
|
||||
def get_queryset(self):
|
||||
""" Return all Build objects (order by date, newest first) """
|
||||
"""Return all Build objects (order by date, newest first)"""
|
||||
return Build.objects.order_by('status', '-completion_date')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['BuildStatus'] = BuildStatus
|
||||
|
||||
context['active'] = self.get_queryset().filter(status__in=BuildStatus.ACTIVE_CODES)
|
||||
|
||||
context['completed'] = self.get_queryset().filter(status=BuildStatus.COMPLETE)
|
||||
context['cancelled'] = self.get_queryset().filter(status=BuildStatus.CANCELLED)
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class BuildDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
|
||||
"""
|
||||
Detail view of a single Build object.
|
||||
"""
|
||||
"""Detail view of a single Build object."""
|
||||
|
||||
model = Build
|
||||
template_name = 'build/detail.html'
|
||||
context_object_name = 'build'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
||||
"""Return extra context information for the BuildDetail view"""
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
|
||||
build = self.get_object()
|
||||
@ -71,9 +52,7 @@ class BuildDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
|
||||
|
||||
|
||||
class BuildDelete(AjaxDeleteView):
|
||||
"""
|
||||
View to delete a build
|
||||
"""
|
||||
"""View to delete a build."""
|
||||
|
||||
model = Build
|
||||
ajax_template_name = 'build/delete_build.html'
|
||||
|
Reference in New Issue
Block a user