mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'docupdates' of https://github.com/matmair/InvenTree into docupdates
This commit is contained in:
commit
6c25872f81
@ -1,3 +1,5 @@
|
|||||||
|
"""Admin functionality for the BuildOrder app"""
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from import_export.admin import ImportExportModelAdmin
|
from import_export.admin import ImportExportModelAdmin
|
||||||
@ -39,6 +41,7 @@ class BuildResource(ModelResource):
|
|||||||
notes = Field(attribute='notes')
|
notes = Field(attribute='notes')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
"""Metaclass options"""
|
||||||
models = Build
|
models = Build
|
||||||
skip_unchanged = True
|
skip_unchanged = True
|
||||||
report_skipped = False
|
report_skipped = False
|
||||||
@ -50,6 +53,7 @@ class BuildResource(ModelResource):
|
|||||||
|
|
||||||
|
|
||||||
class BuildAdmin(ImportExportModelAdmin):
|
class BuildAdmin(ImportExportModelAdmin):
|
||||||
|
"""Class for managing the Build model via the admin interface"""
|
||||||
|
|
||||||
exclude = [
|
exclude = [
|
||||||
'reference_int',
|
'reference_int',
|
||||||
@ -81,6 +85,7 @@ class BuildAdmin(ImportExportModelAdmin):
|
|||||||
|
|
||||||
|
|
||||||
class BuildItemAdmin(admin.ModelAdmin):
|
class BuildItemAdmin(admin.ModelAdmin):
|
||||||
|
"""Class for managing the BuildItem model via the admin interface"""
|
||||||
|
|
||||||
list_display = (
|
list_display = (
|
||||||
'build',
|
'build',
|
||||||
|
@ -27,7 +27,7 @@ class BuildFilter(rest_filters.FilterSet):
|
|||||||
active = rest_filters.BooleanFilter(label='Build is active', method='filter_active')
|
active = rest_filters.BooleanFilter(label='Build is active', method='filter_active')
|
||||||
|
|
||||||
def filter_active(self, queryset, name, value):
|
def filter_active(self, queryset, name, value):
|
||||||
|
"""Filter the queryset to either include or exclude orders which are active"""
|
||||||
if str2bool(value):
|
if str2bool(value):
|
||||||
queryset = queryset.filter(status__in=BuildStatus.ACTIVE_CODES)
|
queryset = queryset.filter(status__in=BuildStatus.ACTIVE_CODES)
|
||||||
else:
|
else:
|
||||||
@ -38,7 +38,7 @@ class BuildFilter(rest_filters.FilterSet):
|
|||||||
overdue = rest_filters.BooleanFilter(label='Build is overdue', method='filter_overdue')
|
overdue = rest_filters.BooleanFilter(label='Build is overdue', method='filter_overdue')
|
||||||
|
|
||||||
def filter_overdue(self, queryset, name, value):
|
def filter_overdue(self, queryset, name, value):
|
||||||
|
"""Filter the queryset to either include or exclude orders which are overdue"""
|
||||||
if str2bool(value):
|
if str2bool(value):
|
||||||
queryset = queryset.filter(Build.OVERDUE_FILTER)
|
queryset = queryset.filter(Build.OVERDUE_FILTER)
|
||||||
else:
|
else:
|
||||||
@ -112,6 +112,7 @@ class BuildList(APIDownloadMixin, generics.ListCreateAPIView):
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def download_queryset(self, queryset, export_format):
|
def download_queryset(self, queryset, export_format):
|
||||||
|
"""Download the queryset data as a file"""
|
||||||
dataset = build.admin.BuildResource().export(queryset=queryset)
|
dataset = build.admin.BuildResource().export(queryset=queryset)
|
||||||
|
|
||||||
filedata = dataset.export(export_format)
|
filedata = dataset.export(export_format)
|
||||||
@ -120,7 +121,7 @@ class BuildList(APIDownloadMixin, generics.ListCreateAPIView):
|
|||||||
return DownloadFile(filedata, filename)
|
return DownloadFile(filedata, filename)
|
||||||
|
|
||||||
def filter_queryset(self, queryset):
|
def filter_queryset(self, queryset):
|
||||||
|
"""Custom query filtering for the BuildList endpoint"""
|
||||||
queryset = super().filter_queryset(queryset)
|
queryset = super().filter_queryset(queryset)
|
||||||
|
|
||||||
params = self.request.query_params
|
params = self.request.query_params
|
||||||
@ -184,7 +185,7 @@ class BuildList(APIDownloadMixin, generics.ListCreateAPIView):
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def get_serializer(self, *args, **kwargs):
|
def get_serializer(self, *args, **kwargs):
|
||||||
|
"""Add extra context information to the endpoint serializer"""
|
||||||
try:
|
try:
|
||||||
part_detail = str2bool(self.request.GET.get('part_detail', None))
|
part_detail = str2bool(self.request.GET.get('part_detail', None))
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@ -215,7 +216,7 @@ class BuildUnallocate(generics.CreateAPIView):
|
|||||||
serializer_class = build.serializers.BuildUnallocationSerializer
|
serializer_class = build.serializers.BuildUnallocationSerializer
|
||||||
|
|
||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
|
"""Add extra context information to the endpoint serializer"""
|
||||||
ctx = super().get_serializer_context()
|
ctx = super().get_serializer_context()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -232,6 +233,7 @@ 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):
|
def get_serializer_context(self):
|
||||||
|
"""Add extra context information to the endpoint serializer"""
|
||||||
ctx = super().get_serializer_context()
|
ctx = super().get_serializer_context()
|
||||||
|
|
||||||
ctx['request'] = self.request
|
ctx['request'] = self.request
|
||||||
@ -265,6 +267,7 @@ class BuildOutputDelete(BuildOrderContextMixin, generics.CreateAPIView):
|
|||||||
"""API endpoint for deleting multiple build outputs."""
|
"""API endpoint for deleting multiple build outputs."""
|
||||||
|
|
||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
|
"""Add extra context information to the endpoint serializer"""
|
||||||
ctx = super().get_serializer_context()
|
ctx = super().get_serializer_context()
|
||||||
|
|
||||||
ctx['to_complete'] = False
|
ctx['to_complete'] = False
|
||||||
@ -338,7 +341,7 @@ class BuildItemList(generics.ListCreateAPIView):
|
|||||||
serializer_class = build.serializers.BuildItemSerializer
|
serializer_class = build.serializers.BuildItemSerializer
|
||||||
|
|
||||||
def get_serializer(self, *args, **kwargs):
|
def get_serializer(self, *args, **kwargs):
|
||||||
|
"""Returns a BuildItemSerializer instance based on the request"""
|
||||||
try:
|
try:
|
||||||
params = self.request.query_params
|
params = self.request.query_params
|
||||||
|
|
||||||
@ -361,7 +364,7 @@ class BuildItemList(generics.ListCreateAPIView):
|
|||||||
return query
|
return query
|
||||||
|
|
||||||
def filter_queryset(self, queryset):
|
def filter_queryset(self, queryset):
|
||||||
|
"""Customm query filtering for the BuildItem list"""
|
||||||
queryset = super().filter_queryset(queryset)
|
queryset = super().filter_queryset(queryset)
|
||||||
|
|
||||||
params = self.request.query_params
|
params = self.request.query_params
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
|
"""Django app for the BuildOrder module"""
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class BuildConfig(AppConfig):
|
class BuildConfig(AppConfig):
|
||||||
|
"""BuildOrder app config class"""
|
||||||
name = 'build'
|
name = 'build'
|
||||||
|
@ -92,10 +92,11 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_api_url():
|
def get_api_url():
|
||||||
|
"""Return the API URL associated with the BuildOrder model"""
|
||||||
return reverse('api-build-list')
|
return reverse('api-build-list')
|
||||||
|
|
||||||
def api_instance_filters(self):
|
def api_instance_filters(self):
|
||||||
|
"""Returns custom API filters for the particular BuildOrder instance"""
|
||||||
return {
|
return {
|
||||||
'parent': {
|
'parent': {
|
||||||
'exclude_tree': self.pk,
|
'exclude_tree': self.pk,
|
||||||
@ -115,7 +116,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
return defaults
|
return defaults
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
"""Custom save method for the BuildOrder model"""
|
||||||
self.rebuild_reference_field()
|
self.rebuild_reference_field()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -126,6 +127,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
})
|
})
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
"""Metaclass options for the BuildOrder model"""
|
||||||
verbose_name = _("Build Order")
|
verbose_name = _("Build Order")
|
||||||
verbose_name_plural = _("Build Orders")
|
verbose_name_plural = _("Build Orders")
|
||||||
|
|
||||||
@ -170,12 +172,13 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
"""String representation of a BuildOrder"""
|
||||||
prefix = getSetting("BUILDORDER_REFERENCE_PREFIX")
|
prefix = getSetting("BUILDORDER_REFERENCE_PREFIX")
|
||||||
|
|
||||||
return f"{prefix}{self.reference}"
|
return f"{prefix}{self.reference}"
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
|
"""Return the web URL associated with this BuildOrder"""
|
||||||
return reverse('build-detail', kwargs={'pk': self.id})
|
return reverse('build-detail', kwargs={'pk': self.id})
|
||||||
|
|
||||||
reference = models.CharField(
|
reference = models.CharField(
|
||||||
@ -393,9 +396,11 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def output_count(self):
|
def output_count(self):
|
||||||
|
"""Return the number of build outputs (StockItem) associated with this build order"""
|
||||||
return self.build_outputs.count()
|
return self.build_outputs.count()
|
||||||
|
|
||||||
def has_build_outputs(self):
|
def has_build_outputs(self):
|
||||||
|
"""Returns True if this build has more than zero build outputs"""
|
||||||
return self.output_count > 0
|
return self.output_count > 0
|
||||||
|
|
||||||
def get_build_outputs(self, **kwargs):
|
def get_build_outputs(self, **kwargs):
|
||||||
@ -436,7 +441,7 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def complete_count(self):
|
def complete_count(self):
|
||||||
|
"""Return the total quantity of completed outputs"""
|
||||||
quantity = 0
|
quantity = 0
|
||||||
|
|
||||||
for output in self.complete_outputs:
|
for output in self.complete_outputs:
|
||||||
@ -1049,6 +1054,7 @@ class BuildOrderAttachment(InvenTreeAttachment):
|
|||||||
"""Model for storing file attachments against a BuildOrder object."""
|
"""Model for storing file attachments against a BuildOrder object."""
|
||||||
|
|
||||||
def getSubdir(self):
|
def getSubdir(self):
|
||||||
|
"""Return the media file subdirectory for storing BuildOrder attachments"""
|
||||||
return os.path.join('bo_files', str(self.build.id))
|
return os.path.join('bo_files', str(self.build.id))
|
||||||
|
|
||||||
build = models.ForeignKey(Build, on_delete=models.CASCADE, related_name='attachments')
|
build = models.ForeignKey(Build, on_delete=models.CASCADE, related_name='attachments')
|
||||||
@ -1069,20 +1075,17 @@ class BuildItem(models.Model):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_api_url():
|
def get_api_url():
|
||||||
|
"""Return the API URL used to access this model"""
|
||||||
return reverse('api-build-item-list')
|
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:
|
class Meta:
|
||||||
|
"""Serializer metaclass"""
|
||||||
unique_together = [
|
unique_together = [
|
||||||
('build', 'stock_item', 'install_into'),
|
('build', 'stock_item', 'install_into'),
|
||||||
]
|
]
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
"""Custom save method for the BuildItem model"""
|
||||||
self.clean()
|
self.clean()
|
||||||
|
|
||||||
super().save()
|
super().save()
|
||||||
|
@ -66,6 +66,7 @@ class BuildSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""Determine if extra serializer fields are required"""
|
||||||
part_detail = kwargs.pop('part_detail', True)
|
part_detail = kwargs.pop('part_detail', True)
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@ -74,6 +75,7 @@ class BuildSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer
|
|||||||
self.fields.pop('part_detail')
|
self.fields.pop('part_detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
"""Serializer metaclass"""
|
||||||
model = Build
|
model = Build
|
||||||
fields = [
|
fields = [
|
||||||
'pk',
|
'pk',
|
||||||
@ -127,7 +129,7 @@ class BuildOutputSerializer(serializers.Serializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def validate_output(self, output):
|
def validate_output(self, output):
|
||||||
|
"""Perform validation for the output (StockItem) provided to the serializer"""
|
||||||
build = self.context['build']
|
build = self.context['build']
|
||||||
|
|
||||||
# As this serializer can be used in multiple contexts, we need to work out why we are here
|
# As this serializer can be used in multiple contexts, we need to work out why we are here
|
||||||
@ -159,6 +161,7 @@ class BuildOutputSerializer(serializers.Serializer):
|
|||||||
return output
|
return output
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
"""Serializer metaclass"""
|
||||||
fields = [
|
fields = [
|
||||||
'output',
|
'output',
|
||||||
]
|
]
|
||||||
@ -182,13 +185,15 @@ class BuildOutputCreateSerializer(serializers.Serializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_build(self):
|
def get_build(self):
|
||||||
|
"""Return the Build instance associated with this serializer"""
|
||||||
return self.context["build"]
|
return self.context["build"]
|
||||||
|
|
||||||
def get_part(self):
|
def get_part(self):
|
||||||
|
"""Return the Part instance associated with the build"""
|
||||||
return self.get_build().part
|
return self.get_build().part
|
||||||
|
|
||||||
def validate_quantity(self, quantity):
|
def validate_quantity(self, quantity):
|
||||||
|
"""Validate the provided quantity field"""
|
||||||
if quantity <= 0:
|
if quantity <= 0:
|
||||||
raise ValidationError(_("Quantity must be greater than zero"))
|
raise ValidationError(_("Quantity must be greater than zero"))
|
||||||
|
|
||||||
@ -219,7 +224,7 @@ class BuildOutputCreateSerializer(serializers.Serializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def validate_serial_numbers(self, serial_numbers):
|
def validate_serial_numbers(self, serial_numbers):
|
||||||
|
"""Clean the provided serial number string"""
|
||||||
serial_numbers = serial_numbers.strip()
|
serial_numbers = serial_numbers.strip()
|
||||||
|
|
||||||
return serial_numbers
|
return serial_numbers
|
||||||
@ -292,6 +297,7 @@ 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:
|
class Meta:
|
||||||
|
"""Serializer metaclass"""
|
||||||
fields = [
|
fields = [
|
||||||
'outputs',
|
'outputs',
|
||||||
]
|
]
|
||||||
@ -302,7 +308,7 @@ class BuildOutputDeleteSerializer(serializers.Serializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
|
"""Perform data validation for this serializer"""
|
||||||
data = super().validate(data)
|
data = super().validate(data)
|
||||||
|
|
||||||
outputs = data.get('outputs', [])
|
outputs = data.get('outputs', [])
|
||||||
@ -329,6 +335,7 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
|
|||||||
"""DRF serializer for completing one or more build outputs."""
|
"""DRF serializer for completing one or more build outputs."""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
"""Serializer metaclass"""
|
||||||
fields = [
|
fields = [
|
||||||
'outputs',
|
'outputs',
|
||||||
'location',
|
'location',
|
||||||
@ -370,7 +377,7 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
|
"""Perform data validation for this serializer"""
|
||||||
super().validate(data)
|
super().validate(data)
|
||||||
|
|
||||||
outputs = data.get('outputs', [])
|
outputs = data.get('outputs', [])
|
||||||
@ -409,15 +416,17 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
|
|
||||||
class BuildCancelSerializer(serializers.Serializer):
|
class BuildCancelSerializer(serializers.Serializer):
|
||||||
|
"""DRF serializer class for cancelling an active BuildOrder"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
"""Serializer metaclass"""
|
||||||
fields = [
|
fields = [
|
||||||
'remove_allocated_stock',
|
'remove_allocated_stock',
|
||||||
'remove_incomplete_outputs',
|
'remove_incomplete_outputs',
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_context_data(self):
|
def get_context_data(self):
|
||||||
|
"""Retrieve extra context data from this serializer"""
|
||||||
build = self.context['build']
|
build = self.context['build']
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -441,7 +450,7 @@ class BuildCancelSerializer(serializers.Serializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
|
"""Cancel the specified build"""
|
||||||
build = self.context['build']
|
build = self.context['build']
|
||||||
request = self.context['request']
|
request = self.context['request']
|
||||||
|
|
||||||
@ -465,7 +474,7 @@ class BuildCompleteSerializer(serializers.Serializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def validate_accept_unallocated(self, value):
|
def validate_accept_unallocated(self, value):
|
||||||
|
"""Check if the 'accept_unallocated' field is required"""
|
||||||
build = self.context['build']
|
build = self.context['build']
|
||||||
|
|
||||||
if not build.are_untracked_parts_allocated() and not value:
|
if not build.are_untracked_parts_allocated() and not value:
|
||||||
@ -481,7 +490,7 @@ class BuildCompleteSerializer(serializers.Serializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def validate_accept_incomplete(self, value):
|
def validate_accept_incomplete(self, value):
|
||||||
|
"""Check if the 'accept_incomplete' field is required"""
|
||||||
build = self.context['build']
|
build = self.context['build']
|
||||||
|
|
||||||
if build.remaining > 0 and not value:
|
if build.remaining > 0 and not value:
|
||||||
@ -490,7 +499,7 @@ class BuildCompleteSerializer(serializers.Serializer):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
|
"""Perform validation of this serializer prior to saving"""
|
||||||
build = self.context['build']
|
build = self.context['build']
|
||||||
|
|
||||||
if build.incomplete_count > 0:
|
if build.incomplete_count > 0:
|
||||||
@ -502,7 +511,7 @@ class BuildCompleteSerializer(serializers.Serializer):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
|
"""Complete the specified build output"""
|
||||||
request = self.context['request']
|
request = self.context['request']
|
||||||
build = self.context['build']
|
build = self.context['build']
|
||||||
|
|
||||||
@ -537,8 +546,7 @@ class BuildUnallocationSerializer(serializers.Serializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def validate_output(self, stock_item):
|
def validate_output(self, stock_item):
|
||||||
|
"""Validation for the output StockItem instance. Stock item must point to the same build order!"""
|
||||||
# Stock item must point to the same build order!
|
|
||||||
build = self.context['build']
|
build = self.context['build']
|
||||||
|
|
||||||
if stock_item and stock_item.build != build:
|
if stock_item and stock_item.build != build:
|
||||||
@ -573,7 +581,7 @@ class BuildAllocationItemSerializer(serializers.Serializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def validate_bom_item(self, bom_item):
|
def validate_bom_item(self, bom_item):
|
||||||
"""Check if the parts match!"""
|
"""Check if the parts match"""
|
||||||
build = self.context['build']
|
build = self.context['build']
|
||||||
|
|
||||||
# BomItem should point to the same 'part' as the parent build
|
# BomItem should point to the same 'part' as the parent build
|
||||||
@ -596,7 +604,7 @@ class BuildAllocationItemSerializer(serializers.Serializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def validate_stock_item(self, stock_item):
|
def validate_stock_item(self, stock_item):
|
||||||
|
"""Perform validation of the stock_item field"""
|
||||||
if not stock_item.in_stock:
|
if not stock_item.in_stock:
|
||||||
raise ValidationError(_("Item must be in stock"))
|
raise ValidationError(_("Item must be in stock"))
|
||||||
|
|
||||||
@ -610,7 +618,7 @@ class BuildAllocationItemSerializer(serializers.Serializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def validate_quantity(self, quantity):
|
def validate_quantity(self, quantity):
|
||||||
|
"""Perform validation of the 'quantity' field"""
|
||||||
if quantity <= 0:
|
if quantity <= 0:
|
||||||
raise ValidationError(_("Quantity must be greater than zero"))
|
raise ValidationError(_("Quantity must be greater than zero"))
|
||||||
|
|
||||||
@ -625,6 +633,7 @@ class BuildAllocationItemSerializer(serializers.Serializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
"""Serializer metaclass"""
|
||||||
fields = [
|
fields = [
|
||||||
'bom_item',
|
'bom_item',
|
||||||
'stock_item',
|
'stock_item',
|
||||||
@ -633,7 +642,7 @@ class BuildAllocationItemSerializer(serializers.Serializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
|
"""Perfofrm data validation for this item"""
|
||||||
super().validate(data)
|
super().validate(data)
|
||||||
|
|
||||||
build = self.context['build']
|
build = self.context['build']
|
||||||
@ -684,6 +693,7 @@ class BuildAllocationSerializer(serializers.Serializer):
|
|||||||
items = BuildAllocationItemSerializer(many=True)
|
items = BuildAllocationItemSerializer(many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
"""Serializer metaclass"""
|
||||||
fields = [
|
fields = [
|
||||||
'items',
|
'items',
|
||||||
]
|
]
|
||||||
@ -700,7 +710,7 @@ class BuildAllocationSerializer(serializers.Serializer):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
|
"""Perform the allocation"""
|
||||||
data = self.validated_data
|
data = self.validated_data
|
||||||
|
|
||||||
items = data.get('items', [])
|
items = data.get('items', [])
|
||||||
@ -732,6 +742,7 @@ 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:
|
class Meta:
|
||||||
|
"""Serializer metaclass"""
|
||||||
fields = [
|
fields = [
|
||||||
'location',
|
'location',
|
||||||
'exclude_location',
|
'exclude_location',
|
||||||
@ -770,7 +781,7 @@ class BuildAutoAllocationSerializer(serializers.Serializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
|
"""Perform the auto-allocation step"""
|
||||||
data = self.validated_data
|
data = self.validated_data
|
||||||
|
|
||||||
build = self.context['build']
|
build = self.context['build']
|
||||||
@ -799,7 +810,7 @@ class BuildItemSerializer(InvenTreeModelSerializer):
|
|||||||
quantity = InvenTreeDecimalField()
|
quantity = InvenTreeDecimalField()
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""Determine which extra details fields should be included"""
|
||||||
build_detail = kwargs.pop('build_detail', False)
|
build_detail = kwargs.pop('build_detail', False)
|
||||||
part_detail = kwargs.pop('part_detail', False)
|
part_detail = kwargs.pop('part_detail', False)
|
||||||
location_detail = kwargs.pop('location_detail', False)
|
location_detail = kwargs.pop('location_detail', False)
|
||||||
@ -816,6 +827,7 @@ class BuildItemSerializer(InvenTreeModelSerializer):
|
|||||||
self.fields.pop('location_detail')
|
self.fields.pop('location_detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
"""Serializer metaclass"""
|
||||||
model = BuildItem
|
model = BuildItem
|
||||||
fields = [
|
fields = [
|
||||||
'pk',
|
'pk',
|
||||||
@ -837,6 +849,7 @@ class BuildAttachmentSerializer(InvenTreeAttachmentSerializer):
|
|||||||
"""Serializer for a BuildAttachment."""
|
"""Serializer for a BuildAttachment."""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
"""Serializer metaclass"""
|
||||||
model = BuildOrderAttachment
|
model = BuildOrderAttachment
|
||||||
|
|
||||||
fields = [
|
fields = [
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
"""Background task definitions for the BuildOrder app"""
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
"""Unit tests for the BuildOrder API"""
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -91,16 +93,12 @@ class BuildAPITest(InvenTreeAPITestCase):
|
|||||||
'build.add'
|
'build.add'
|
||||||
]
|
]
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
|
|
||||||
super().setUp()
|
|
||||||
|
|
||||||
|
|
||||||
class BuildTest(BuildAPITest):
|
class BuildTest(BuildAPITest):
|
||||||
"""Unit testing for the build complete API endpoint."""
|
"""Unit testing for the build complete API endpoint."""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
"""Basic setup for this test suite"""
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
self.build = Build.objects.get(pk=1)
|
self.build = Build.objects.get(pk=1)
|
||||||
@ -477,7 +475,7 @@ class BuildTest(BuildAPITest):
|
|||||||
self.assertIn('This build output has already been completed', str(response.data))
|
self.assertIn('This build output has already been completed', str(response.data))
|
||||||
|
|
||||||
def test_download_build_orders(self):
|
def test_download_build_orders(self):
|
||||||
|
"""Test that we can download a list of build orders via the API"""
|
||||||
required_cols = [
|
required_cols = [
|
||||||
'reference',
|
'reference',
|
||||||
'status',
|
'status',
|
||||||
@ -532,7 +530,7 @@ class BuildAllocationTest(BuildAPITest):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
"""Basic operation as part of test suite setup"""
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
self.assignRole('build.add')
|
self.assignRole('build.add')
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
"""Unit tests for the BuildOrder app"""
|
||||||
|
|
||||||
from ctypes import Union
|
from ctypes import Union
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
@ -114,6 +116,7 @@ class BuildTestBase(TestCase):
|
|||||||
|
|
||||||
|
|
||||||
class BuildTest(BuildTestBase):
|
class BuildTest(BuildTestBase):
|
||||||
|
"""Basic set of tests for the Build model"""
|
||||||
|
|
||||||
def test_ref_int(self):
|
def test_ref_int(self):
|
||||||
"""Test the "integer reference" field used for natural sorting."""
|
"""Test the "integer reference" field used for natural sorting."""
|
||||||
@ -133,8 +136,7 @@ class BuildTest(BuildTestBase):
|
|||||||
self.assertEqual(build.reference_int, ii)
|
self.assertEqual(build.reference_int, ii)
|
||||||
|
|
||||||
def test_init(self):
|
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)
|
self.assertEqual(StockItem.objects.count(), 10)
|
||||||
|
|
||||||
# Build is PENDING
|
# Build is PENDING
|
||||||
@ -158,8 +160,7 @@ class BuildTest(BuildTestBase):
|
|||||||
self.assertFalse(self.build.is_complete)
|
self.assertFalse(self.build.is_complete)
|
||||||
|
|
||||||
def test_build_item_clean(self):
|
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)
|
stock = StockItem.objects.create(part=self.assembly, quantity=99)
|
||||||
|
|
||||||
# Create a BuiltItem which points to an invalid StockItem
|
# Create a BuiltItem which points to an invalid StockItem
|
||||||
@ -185,8 +186,7 @@ class BuildTest(BuildTestBase):
|
|||||||
b.save()
|
b.save()
|
||||||
|
|
||||||
def test_duplicate_bom_line(self):
|
def test_duplicate_bom_line(self):
|
||||||
# Try to add a duplicate BOM item - it should fail!
|
"""Try to add a duplicate BOM item - it should fail!"""
|
||||||
|
|
||||||
with self.assertRaises(IntegrityError):
|
with self.assertRaises(IntegrityError):
|
||||||
BomItem.objects.create(
|
BomItem.objects.create(
|
||||||
part=self.assembly,
|
part=self.assembly,
|
||||||
@ -291,7 +291,7 @@ class BuildTest(BuildTestBase):
|
|||||||
|
|
||||||
self.assertEqual(BuildItem.objects.count(), 0)
|
self.assertEqual(BuildItem.objects.count(), 0)
|
||||||
"""
|
"""
|
||||||
pass
|
...
|
||||||
|
|
||||||
def test_complete(self):
|
def test_complete(self):
|
||||||
"""Test completion of a build output."""
|
"""Test completion of a build output."""
|
||||||
@ -370,7 +370,7 @@ class AutoAllocationTests(BuildTestBase):
|
|||||||
"""Tests for auto allocating stock against a build order."""
|
"""Tests for auto allocating stock against a build order."""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
"""Create some data as part of this test suite"""
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
# Add a "substitute" part for bom_item_2
|
# Add a "substitute" part for bom_item_2
|
||||||
|
@ -38,7 +38,7 @@ class TestForwardMigrations(MigratorTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_items_exist(self):
|
def test_items_exist(self):
|
||||||
|
"""Test to ensure that the 'assembly' field is correctly configured"""
|
||||||
Part = self.new_state.apps.get_model('part', 'part')
|
Part = self.new_state.apps.get_model('part', 'part')
|
||||||
|
|
||||||
self.assertEqual(Part.objects.count(), 1)
|
self.assertEqual(Part.objects.count(), 1)
|
||||||
@ -96,7 +96,7 @@ class TestReferenceMigration(MigratorTestCase):
|
|||||||
print(build.reference)
|
print(build.reference)
|
||||||
|
|
||||||
def test_build_reference(self):
|
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')
|
Build = self.new_state.apps.get_model('build', 'build')
|
||||||
|
|
||||||
self.assertEqual(Build.objects.count(), 3)
|
self.assertEqual(Build.objects.count(), 3)
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
"""Basic unit tests for the BuildOrder app"""
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
@ -11,6 +13,7 @@ from InvenTree.status_codes import BuildStatus
|
|||||||
|
|
||||||
|
|
||||||
class BuildTestSimple(InvenTreeTestCase):
|
class BuildTestSimple(InvenTreeTestCase):
|
||||||
|
"""Basic set of tests for the BuildOrder model functionality"""
|
||||||
|
|
||||||
fixtures = [
|
fixtures = [
|
||||||
'category',
|
'category',
|
||||||
@ -26,7 +29,7 @@ class BuildTestSimple(InvenTreeTestCase):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def test_build_objects(self):
|
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)
|
self.assertEqual(Build.objects.count(), 5)
|
||||||
b = Build.objects.get(pk=2)
|
b = Build.objects.get(pk=2)
|
||||||
self.assertEqual(b.batch, 'B2')
|
self.assertEqual(b.batch, 'B2')
|
||||||
@ -35,10 +38,12 @@ class BuildTestSimple(InvenTreeTestCase):
|
|||||||
self.assertEqual(str(b), 'BO0002')
|
self.assertEqual(str(b), 'BO0002')
|
||||||
|
|
||||||
def test_url(self):
|
def test_url(self):
|
||||||
|
"""Test URL lookup"""
|
||||||
b1 = Build.objects.get(pk=1)
|
b1 = Build.objects.get(pk=1)
|
||||||
self.assertEqual(b1.get_absolute_url(), '/build/1/')
|
self.assertEqual(b1.get_absolute_url(), '/build/1/')
|
||||||
|
|
||||||
def test_is_complete(self):
|
def test_is_complete(self):
|
||||||
|
"""Test build completion status"""
|
||||||
b1 = Build.objects.get(pk=1)
|
b1 = Build.objects.get(pk=1)
|
||||||
b2 = Build.objects.get(pk=2)
|
b2 = Build.objects.get(pk=2)
|
||||||
|
|
||||||
@ -63,6 +68,7 @@ class BuildTestSimple(InvenTreeTestCase):
|
|||||||
self.assertFalse(build.is_overdue)
|
self.assertFalse(build.is_overdue)
|
||||||
|
|
||||||
def test_is_active(self):
|
def test_is_active(self):
|
||||||
|
"""Test active / inactive build status"""
|
||||||
b1 = Build.objects.get(pk=1)
|
b1 = Build.objects.get(pk=1)
|
||||||
b2 = Build.objects.get(pk=2)
|
b2 = Build.objects.get(pk=2)
|
||||||
|
|
||||||
@ -70,8 +76,9 @@ class BuildTestSimple(InvenTreeTestCase):
|
|||||||
self.assertEqual(b2.is_active, False)
|
self.assertEqual(b2.is_active, False)
|
||||||
|
|
||||||
def test_required_parts(self):
|
def test_required_parts(self):
|
||||||
# TODO - Generate BOM for test part
|
"""Test set of required BOM items for the build"""
|
||||||
pass
|
# TODO: Generate BOM for test part
|
||||||
|
...
|
||||||
|
|
||||||
def test_cancel_build(self):
|
def test_cancel_build(self):
|
||||||
"""Test build cancellation function."""
|
"""Test build cancellation function."""
|
||||||
@ -101,6 +108,7 @@ class TestBuildViews(InvenTreeTestCase):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
"""Fixturing for this suite of unit tests"""
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
# Create a build output for build # 1
|
# Create a build output for build # 1
|
||||||
|
@ -22,19 +22,6 @@ class BuildIndex(InvenTreeRoleMixin, ListView):
|
|||||||
"""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')
|
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):
|
class BuildDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
|
||||||
"""Detail view of a single Build object."""
|
"""Detail view of a single Build object."""
|
||||||
@ -44,7 +31,7 @@ class BuildDetail(InvenTreeRoleMixin, InvenTreePluginViewMixin, DetailView):
|
|||||||
context_object_name = 'build'
|
context_object_name = 'build'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
"""Return extra context information for the BuildDetail view"""
|
||||||
ctx = super().get_context_data(**kwargs)
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
build = self.get_object()
|
build = self.get_object()
|
||||||
|
Loading…
Reference in New Issue
Block a user