2
0
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 merge 99676ee

* 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 commit ca96654622.

* use typing here

* Revert "Make API code cleaner"

This reverts commit 24fb68bd3e.

* 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:
Matthias Mair
2022-06-01 17:37:39 +02:00
committed by GitHub
parent 66a6915213
commit 0c97a50e47
223 changed files with 4416 additions and 6980 deletions

View File

@ -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
"""

View File

@ -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',

View File

@ -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

View File

@ -1,5 +1,8 @@
"""Django app for the BuildOrder module"""
from django.apps import AppConfig
class BuildConfig(AppConfig):
"""BuildOrder app config class"""
name = 'build'

View File

@ -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:

View File

@ -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 = [

View File

@ -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

View File

@ -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)

View File

@ -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,

View File

@ -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)

View File

@ -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,)))

View File

@ -1,6 +1,4 @@
"""
URL lookup for Build app
"""
"""URL lookup for Build app."""
from django.urls import include, re_path

View File

@ -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'