Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Build order cancel #7153

Merged
merged 11 commits into from
May 4, 2024
5 changes: 4 additions & 1 deletion src/backend/InvenTree/InvenTree/api_version.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
"""InvenTree API version information."""

# InvenTree API version
INVENTREE_API_VERSION = 194
INVENTREE_API_VERSION = 195
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""

INVENTREE_API_TEXT = """

v195 - 2024-05-03 : https://github.com/inventree/InvenTree/pull/7153
- Fixes bug in BuildOrderCancel API endpoint

v194 - 2024-05-01 : https://github.com/inventree/InvenTree/pull/7147
- Adds field description to the currency_exchange_retrieve API call

Expand Down
2 changes: 1 addition & 1 deletion src/backend/InvenTree/InvenTree/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@
},
'IGNORE_REQUEST_PATTERNS': ['^(?!\/(api)?(plugin)?\/).*'],
'IGNORE_SQL_PATTERNS': [],
'DISPLAY_DUPLICATES': 3,
'DISPLAY_DUPLICATES': 1,
'RESPONSE_HEADER': 'X-Django-Query-Count',
}

Expand Down
31 changes: 24 additions & 7 deletions src/backend/InvenTree/build/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,15 +103,35 @@ def filter_has_project_code(self, queryset, name, value):
return queryset.filter(project_code=None)


class BuildList(APIDownloadMixin, ListCreateAPI):
class BuildMixin:
"""Mixin class for Build API endpoints."""

queryset = Build.objects.all()
serializer_class = build.serializers.BuildSerializer

def get_queryset(self):
"""Return the queryset for the Build API endpoints."""
queryset = super().get_queryset()

queryset = queryset.prefetch_related(
'responsible',
'issued_by',
'build_lines',
'build_lines__bom_item',
'build_lines__build',
'part',
)

return queryset


class BuildList(APIDownloadMixin, BuildMixin, ListCreateAPI):
"""API endpoint for accessing a list of Build objects.

- GET: Return list of objects (with filters)
- POST: Create a new Build object
"""

queryset = Build.objects.all()
serializer_class = build.serializers.BuildSerializer
filterset_class = BuildFilter

filter_backends = SEARCH_ORDER_FILTER_ALIAS
Expand Down Expand Up @@ -223,12 +243,9 @@ def get_serializer(self, *args, **kwargs):
return self.serializer_class(*args, **kwargs)


class BuildDetail(RetrieveUpdateDestroyAPI):
class BuildDetail(BuildMixin, RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a Build object."""

queryset = Build.objects.all()
serializer_class = build.serializers.BuildSerializer

def destroy(self, request, *args, **kwargs):
"""Only allow deletion of a BuildOrder if the build status is CANCELLED"""
build = self.get_object()
Expand Down
26 changes: 16 additions & 10 deletions src/backend/InvenTree/build/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,11 +552,12 @@
self.save()

# Offload task to complete build allocations
InvenTree.tasks.offload_task(
if not InvenTree.tasks.offload_task(
build.tasks.complete_build_allocations,
self.pk,
user.pk if user else None
)
):
raise ValidationError(_("Failed to offload task to complete build allocations"))

Check warning on line 560 in src/backend/InvenTree/build/models.py

View check run for this annotation

Codecov / codecov/patch

src/backend/InvenTree/build/models.py#L560

Added line #L560 was not covered by tests

# Register an event
trigger_event('build.completed', id=self.pk)
Expand Down Expand Up @@ -608,24 +609,29 @@
- Set build status to CANCELLED
- Save the Build object
"""

import build.tasks

remove_allocated_stock = kwargs.get('remove_allocated_stock', False)
remove_incomplete_outputs = kwargs.get('remove_incomplete_outputs', False)

# Find all BuildItem objects associated with this Build
items = self.allocated_stock

if remove_allocated_stock:
for item in items:
item.complete_allocation(user)
# Offload task to remove allocated stock
if not InvenTree.tasks.offload_task(
build.tasks.complete_build_allocations,
self.pk,
user.pk if user else None
):
raise ValidationError(_("Failed to offload task to complete build allocations"))

Check warning on line 625 in src/backend/InvenTree/build/models.py

View check run for this annotation

Codecov / codecov/patch

src/backend/InvenTree/build/models.py#L625

Added line #L625 was not covered by tests

items.delete()
else:
self.allocated_stock.all().delete()

# Remove incomplete outputs (if required)
if remove_incomplete_outputs:
outputs = self.build_outputs.filter(is_building=True)

for output in outputs:
output.delete()
outputs.delete()

Check warning on line 634 in src/backend/InvenTree/build/models.py

View check run for this annotation

Codecov / codecov/patch

src/backend/InvenTree/build/models.py#L634

Added line #L634 was not covered by tests

# Date of 'completion' is the date the build was cancelled
self.completion_date = InvenTree.helpers.current_date()
Expand Down
25 changes: 15 additions & 10 deletions src/backend/InvenTree/build/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,12 @@
from rest_framework import serializers
from rest_framework.serializers import ValidationError

from sql_util.utils import SubquerySum

from InvenTree.serializers import InvenTreeModelSerializer, InvenTreeAttachmentSerializer
from InvenTree.serializers import UserSerializer

import InvenTree.helpers
from InvenTree.serializers import InvenTreeDecimalField
from InvenTree.status_codes import BuildStatusGroups, StockStatus
from InvenTree.status_codes import StockStatus

from stock.models import generate_batch_code, StockItem, StockLocation
from stock.serializers import StockItemSerializerBrief, LocationSerializer
Expand Down Expand Up @@ -589,8 +587,8 @@
}

remove_allocated_stock = serializers.BooleanField(
label=_('Remove Allocated Stock'),
help_text=_('Subtract any stock which has already been allocated to this build'),
label=_('Consume Allocated Stock'),
help_text=_('Consume any stock which has already been allocated to this build'),
required=False,
default=False,
)
Expand All @@ -611,7 +609,7 @@

build.cancel_build(
request.user,
remove_allocated_stock=data.get('remove_unallocated_stock', False),
remove_allocated_stock=data.get('remove_allocated_stock', False),
remove_incomplete_outputs=data.get('remove_incomplete_outputs', False),
)

Expand Down Expand Up @@ -994,17 +992,24 @@

def save(self):
"""Perform the auto-allocation step"""

import build.tasks
import InvenTree.tasks

Check warning on line 997 in src/backend/InvenTree/build/serializers.py

View check run for this annotation

Codecov / codecov/patch

src/backend/InvenTree/build/serializers.py#L996-L997

Added lines #L996 - L997 were not covered by tests

data = self.validated_data

build = self.context['build']
build_order = self.context['build']

Check warning on line 1001 in src/backend/InvenTree/build/serializers.py

View check run for this annotation

Codecov / codecov/patch

src/backend/InvenTree/build/serializers.py#L1001

Added line #L1001 was not covered by tests

build.auto_allocate_stock(
if not InvenTree.tasks.offload_task(

Check warning on line 1003 in src/backend/InvenTree/build/serializers.py

View check run for this annotation

Codecov / codecov/patch

src/backend/InvenTree/build/serializers.py#L1003

Added line #L1003 was not covered by tests
build.tasks.auto_allocate_build,
build_order.pk,
location=data.get('location', None),
exclude_location=data.get('exclude_location', None),
interchangeable=data['interchangeable'],
substitutes=data['substitutes'],
optional_items=data['optional_items'],
)
optional_items=data['optional_items']
):
raise ValidationError(_("Failed to start auto-allocation task"))

Check warning on line 1012 in src/backend/InvenTree/build/serializers.py

View check run for this annotation

Codecov / codecov/patch

src/backend/InvenTree/build/serializers.py#L1012

Added line #L1012 was not covered by tests


class BuildItemSerializer(InvenTreeModelSerializer):
Expand Down
12 changes: 12 additions & 0 deletions src/backend/InvenTree/build/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@
logger = logging.getLogger('inventree')


def auto_allocate_build(build_id: int, **kwargs):
"""Run auto-allocation for a specified BuildOrder."""

build_order = build.models.Build.objects.filter(pk=build_id).first()

if not build_order:
logger.warning("Could not auto-allocate BuildOrder <%s> - BuildOrder does not exist", build_id)
return

build_order.auto_allocate_stock(**kwargs)


def complete_build_allocations(build_id: int, user_id: int):
"""Complete build allocations for a specified BuildOrder."""

Expand Down
48 changes: 46 additions & 2 deletions src/backend/InvenTree/build/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,35 @@ def test_complete(self):
self.assertTrue(self.build.is_complete)

def test_cancel(self):
"""Test that we can cancel a BuildOrder via the API."""
bo = Build.objects.get(pk=1)
"""Test that we can cancel a BuildOrder via the API.

- First test that all stock is returned to stock
- Second test that stock is consumed by the build order
"""

def make_new_build(ref):
"""Make a new build order, and allocate stock to it."""

data = self.post(
reverse('api-build-list'),
{
'part': 100,
'quantity': 10,
'title': 'Test build',
'reference': ref,
},
expected_code=201
).data

build = Build.objects.get(pk=data['pk'])

build.auto_allocate_stock()

self.assertGreater(build.build_lines.count(), 0)

return build

bo = make_new_build('BO-12345')

url = reverse('api-build-cancel', kwargs={'pk': bo.pk})

Expand All @@ -277,6 +304,23 @@ def test_cancel(self):

self.assertEqual(bo.status, BuildStatus.CANCELLED)

# No items were "consumed" by this build
self.assertEqual(bo.consumed_stock.count(), 0)

# Make another build, this time we will *consume* the allocated stock
bo = make_new_build('BO-12346')

url = reverse('api-build-cancel', kwargs={'pk': bo.pk})

self.post(url, {'remove_allocated_stock': True}, expected_code=201)

bo.refresh_from_db()

self.assertEqual(bo.status, BuildStatus.CANCELLED)

# This time, there should be *consumed* stock
self.assertGreater(bo.consumed_stock.count(), 0)

def test_delete(self):
"""Test that we can delete a BuildOrder via the API"""
bo = Build.objects.get(pk=1)
Expand Down
10 changes: 6 additions & 4 deletions src/backend/InvenTree/templates/js/translated/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -974,11 +974,13 @@ function loadBuildOrderAllocationTable(table, options={}) {
let ref = row.build_detail?.reference ?? row.build;
let html = renderLink(ref, `/build/${row.build}/`);

html += `- <small>${row.build_detail.title}</small>`;
if (row.build_detail) {
html += `- <small>${row.build_detail.title}</small>`;

html += buildStatusDisplay(row.build_detail.status, {
classes: 'float-right',
});
html += buildStatusDisplay(row.build_detail.status, {
classes: 'float-right',
});
}

return html;
}
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/src/enums/ApiEndpoints.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@ export enum ApiEndpoints {

// Build API endpoints
build_order_list = 'build/',
build_order_cancel = 'build/:id/cancel/',
build_order_attachment_list = 'build/attachment/',
build_line_list = 'build/line/',

bom_list = 'bom/',

// Part API endpoints
Expand Down
17 changes: 16 additions & 1 deletion src/frontend/src/pages/build/BuildDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,18 @@
}
});

const cancelBuild = useCreateApiFormModal({
url: apiUrl(ApiEndpoints.build_order_cancel, build.pk),
title: t`Cancel Build Order`,
fields: {
remove_allocated_stock: {},
remove_incomplete_outputs: {}
},
onFormSuccess: () => {
refreshInstance();

Check warning on line 312 in src/frontend/src/pages/build/BuildDetail.tsx

View check run for this annotation

Codecov / codecov/patch

src/frontend/src/pages/build/BuildDetail.tsx#L312

Added line #L312 was not covered by tests
}
});

const duplicateBuild = useCreateApiFormModal({
url: ApiEndpoints.build_order_list,
title: t`Add Build Order`,
Expand Down Expand Up @@ -352,7 +364,9 @@
hidden: !user.hasChangeRole(UserRoles.build)
}),
CancelItemAction({
tooltip: t`Cancel order`
tooltip: t`Cancel order`,
onClick: () => cancelBuild.open()

Check warning on line 368 in src/frontend/src/pages/build/BuildDetail.tsx

View check run for this annotation

Codecov / codecov/patch

src/frontend/src/pages/build/BuildDetail.tsx#L368

Added line #L368 was not covered by tests
// TODO: Hide if build cannot be cancelled
}),
DuplicateItemAction({
onClick: () => duplicateBuild.open(),
Expand All @@ -379,6 +393,7 @@
<>
{editBuild.modal}
{duplicateBuild.modal}
{cancelBuild.modal}
<Stack spacing="xs">
<LoadingOverlay visible={instanceQuery.isFetching} />
<PageDetail
Expand Down
Loading