mirror of
https://github.com/inventree/InvenTree.git
synced 2026-05-28 03:49:20 +00:00
Offload build output functions:
- cancel output - scrap output - complete output Perform these in the background worker, and monitor for progress on the frontend.
This commit is contained in:
@@ -37,7 +37,7 @@ def isAppLoaded(app_name: str) -> bool:
|
||||
|
||||
def isInTestMode():
|
||||
"""Returns True if the database is in testing mode."""
|
||||
return 'test' in sys.argv or sys.argv[0].endswith('pytest')
|
||||
return any(x in sys.argv for x in ['test', 'pytest'])
|
||||
|
||||
|
||||
def isWaitingForDatabase():
|
||||
|
||||
@@ -735,14 +735,81 @@ class BuildOutputScrap(BuildOrderContextMixin, CreateAPI):
|
||||
ctx['to_complete'] = False
|
||||
return ctx
|
||||
|
||||
@extend_schema(responses={200: common.serializers.TaskDetailSerializer})
|
||||
def post(self, *args, **kwargs):
|
||||
"""Override POST to offload scrapping to the background worker."""
|
||||
from build.tasks import scrap_build_outputs
|
||||
from InvenTree.tasks import offload_task
|
||||
|
||||
build = self.get_build()
|
||||
serializer = self.get_serializer(data=self.request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
data = serializer.validated_data
|
||||
|
||||
task_id = offload_task(
|
||||
scrap_build_outputs,
|
||||
build.pk,
|
||||
outputs=[
|
||||
{
|
||||
'output_id': item['output'].pk,
|
||||
'quantity': float(item['quantity'])
|
||||
if item.get('quantity') is not None
|
||||
else None,
|
||||
}
|
||||
for item in data['outputs']
|
||||
],
|
||||
location_id=data['location'].pk,
|
||||
notes=data.get('notes', ''),
|
||||
discard_allocations=data.get('discard_allocations', False),
|
||||
user_id=self.request.user.pk,
|
||||
group='build',
|
||||
)
|
||||
|
||||
response = common.serializers.TaskDetailSerializer.from_task(task_id).data
|
||||
return Response(response, status=response['http_status'])
|
||||
|
||||
|
||||
class BuildOutputComplete(BuildOrderContextMixin, CreateAPI):
|
||||
"""API endpoint for completing build outputs."""
|
||||
|
||||
queryset = Build.objects.none()
|
||||
|
||||
serializer_class = build.serializers.BuildOutputCompleteSerializer
|
||||
|
||||
@extend_schema(responses={200: common.serializers.TaskDetailSerializer})
|
||||
def post(self, *args, **kwargs):
|
||||
"""Override POST to offload build output completion to the background worker."""
|
||||
from build.tasks import complete_build_outputs
|
||||
from InvenTree.tasks import offload_task
|
||||
|
||||
build = self.get_build()
|
||||
serializer = self.get_serializer(data=self.request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
data = serializer.validated_data
|
||||
|
||||
location = data.get('location')
|
||||
|
||||
task_id = offload_task(
|
||||
complete_build_outputs,
|
||||
build.pk,
|
||||
outputs=[
|
||||
{
|
||||
'output_id': item['output'].pk,
|
||||
'quantity': float(item['quantity'])
|
||||
if item.get('quantity') is not None
|
||||
else None,
|
||||
}
|
||||
for item in data['outputs']
|
||||
],
|
||||
location_id=location.pk if location else None,
|
||||
status=data.get('status_custom_key'),
|
||||
notes=data.get('notes', ''),
|
||||
user_id=self.request.user.pk,
|
||||
group='build',
|
||||
)
|
||||
|
||||
response = common.serializers.TaskDetailSerializer.from_task(task_id).data
|
||||
return Response(response, status=response['http_status'])
|
||||
|
||||
|
||||
class BuildOutputDelete(BuildOrderContextMixin, CreateAPI):
|
||||
"""API endpoint for deleting multiple build outputs."""
|
||||
@@ -750,15 +817,33 @@ class BuildOutputDelete(BuildOrderContextMixin, CreateAPI):
|
||||
def get_serializer_context(self):
|
||||
"""Add extra context information to the endpoint serializer."""
|
||||
ctx = super().get_serializer_context()
|
||||
|
||||
ctx['to_complete'] = False
|
||||
|
||||
return ctx
|
||||
|
||||
queryset = Build.objects.none()
|
||||
|
||||
serializer_class = build.serializers.BuildOutputDeleteSerializer
|
||||
|
||||
@extend_schema(responses={200: common.serializers.TaskDetailSerializer})
|
||||
def post(self, *args, **kwargs):
|
||||
"""Override POST to offload build output deletion to the background worker."""
|
||||
from build.tasks import delete_build_outputs
|
||||
from InvenTree.tasks import offload_task
|
||||
|
||||
build = self.get_build()
|
||||
serializer = self.get_serializer(data=self.request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
data = serializer.validated_data
|
||||
|
||||
task_id = offload_task(
|
||||
delete_build_outputs,
|
||||
build.pk,
|
||||
output_ids=[item['output'].pk for item in data['outputs']],
|
||||
group='build',
|
||||
)
|
||||
|
||||
response = common.serializers.TaskDetailSerializer.from_task(task_id).data
|
||||
return Response(response, status=response['http_status'])
|
||||
|
||||
|
||||
class BuildFinish(BuildOrderContextMixin, CreateAPI):
|
||||
"""API endpoint for marking a build as finished (completed)."""
|
||||
|
||||
@@ -462,18 +462,6 @@ class BuildOutputDeleteSerializer(serializers.Serializer):
|
||||
|
||||
return data
|
||||
|
||||
def save(self):
|
||||
"""'save' the serializer to delete the build outputs."""
|
||||
data = self.validated_data
|
||||
outputs = data.get('outputs', [])
|
||||
|
||||
build = self.context['build']
|
||||
|
||||
with transaction.atomic():
|
||||
for item in outputs:
|
||||
output = item['output']
|
||||
build.delete_output(output)
|
||||
|
||||
|
||||
class BuildOutputScrapSerializer(serializers.Serializer):
|
||||
"""Scrapping one or more build outputs."""
|
||||
@@ -518,27 +506,6 @@ class BuildOutputScrapSerializer(serializers.Serializer):
|
||||
|
||||
return data
|
||||
|
||||
def save(self):
|
||||
"""Save the serializer to scrap the build outputs."""
|
||||
build = self.context['build']
|
||||
request = self.context.get('request')
|
||||
data = self.validated_data
|
||||
outputs = data.get('outputs', [])
|
||||
|
||||
# Scrap the build outputs
|
||||
with transaction.atomic():
|
||||
for item in outputs:
|
||||
output = item['output']
|
||||
quantity = item.get('quantity', None)
|
||||
build.scrap_build_output(
|
||||
output,
|
||||
quantity,
|
||||
data.get('location', None),
|
||||
user=request.user if request else None,
|
||||
notes=data.get('notes', ''),
|
||||
discard_allocations=data.get('discard_allocations', False),
|
||||
)
|
||||
|
||||
|
||||
class BuildOutputCompleteSerializer(serializers.Serializer):
|
||||
"""DRF serializer for completing one or more build outputs."""
|
||||
@@ -610,42 +577,6 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
|
||||
|
||||
return data
|
||||
|
||||
def save(self):
|
||||
"""Save the serializer to complete the build outputs."""
|
||||
build = self.context['build']
|
||||
request = self.context.get('request')
|
||||
|
||||
data = self.validated_data
|
||||
|
||||
location = data.get('location', None)
|
||||
status = data.get('status_custom_key', StockStatus.OK.value)
|
||||
notes = data.get('notes', '')
|
||||
|
||||
outputs = data.get('outputs', [])
|
||||
|
||||
# Cache some calculated values which can be passed to each output
|
||||
required_tests = outputs[0]['output'].part.getRequiredTests()
|
||||
prevent_on_incomplete = (
|
||||
common.settings.prevent_build_output_complete_on_incompleted_tests()
|
||||
)
|
||||
|
||||
# Mark the specified build outputs as "complete"
|
||||
with transaction.atomic():
|
||||
for item in outputs:
|
||||
output = item['output']
|
||||
quantity = item.get('quantity', None)
|
||||
|
||||
build.complete_build_output(
|
||||
output,
|
||||
request.user if request else None,
|
||||
quantity=quantity,
|
||||
location=location,
|
||||
status=status,
|
||||
notes=notes,
|
||||
required_tests=required_tests,
|
||||
prevent_on_incomplete=prevent_on_incomplete,
|
||||
)
|
||||
|
||||
|
||||
class BuildIssueSerializer(serializers.Serializer):
|
||||
"""DRF serializer for issuing a build order."""
|
||||
|
||||
@@ -100,6 +100,113 @@ def complete_build_allocations(build_id: int, user_id: int):
|
||||
build_order.complete_allocations(user)
|
||||
|
||||
|
||||
@tracer.start_as_current_span('delete_build_outputs')
|
||||
def delete_build_outputs(build_id: int, output_ids: list, **kwargs):
|
||||
"""Delete (cancel) specified build outputs for a BuildOrder.
|
||||
|
||||
Arguments:
|
||||
build_id: The ID of the BuildOrder
|
||||
output_ids: List of StockItem PKs to delete
|
||||
"""
|
||||
from build.models import Build
|
||||
from stock.models import StockItem
|
||||
|
||||
build = Build.objects.get(pk=build_id)
|
||||
|
||||
with transaction.atomic():
|
||||
for output_id in output_ids:
|
||||
output = StockItem.objects.filter(pk=output_id).first()
|
||||
if output:
|
||||
build.delete_output(output)
|
||||
|
||||
|
||||
@tracer.start_as_current_span('scrap_build_outputs')
|
||||
def scrap_build_outputs(
|
||||
build_id: int,
|
||||
outputs: list,
|
||||
location_id: int,
|
||||
notes: str = '',
|
||||
discard_allocations: bool = False,
|
||||
user_id: int | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Scrap specified build outputs for a BuildOrder.
|
||||
|
||||
Arguments:
|
||||
build_id: The ID of the BuildOrder
|
||||
outputs: List of dicts with 'output_id' and 'quantity'
|
||||
location_id: PK of the destination StockLocation
|
||||
notes: Reason for scrapping
|
||||
discard_allocations: If True, discard (not consume) allocations
|
||||
user_id: PK of the user initiating the action
|
||||
"""
|
||||
from build.models import Build
|
||||
from stock.models import StockItem, StockLocation
|
||||
|
||||
build = Build.objects.get(pk=build_id)
|
||||
location = StockLocation.objects.get(pk=location_id)
|
||||
user = User.objects.filter(pk=user_id).first() if user_id else None
|
||||
|
||||
with transaction.atomic():
|
||||
for item in outputs:
|
||||
output = StockItem.objects.filter(pk=item['output_id']).first()
|
||||
if output:
|
||||
build.scrap_build_output(
|
||||
output,
|
||||
item.get('quantity'),
|
||||
location,
|
||||
user=user,
|
||||
notes=notes,
|
||||
discard_allocations=discard_allocations,
|
||||
)
|
||||
|
||||
|
||||
@tracer.start_as_current_span('complete_build_outputs')
|
||||
def complete_build_outputs(
|
||||
build_id: int,
|
||||
outputs: list,
|
||||
location_id: int | None,
|
||||
status: int,
|
||||
notes: str = '',
|
||||
user_id: int | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Complete specified build outputs for a BuildOrder.
|
||||
|
||||
Arguments:
|
||||
build_id: The ID of the BuildOrder
|
||||
outputs: List of dicts with 'output_id' and optional 'quantity'
|
||||
location_id: PK of the destination StockLocation (or None)
|
||||
status: Stock status code to assign to completed outputs
|
||||
notes: Completion notes
|
||||
user_id: PK of the user initiating the action
|
||||
"""
|
||||
from build.models import Build
|
||||
from stock.models import StockItem, StockLocation
|
||||
|
||||
build = Build.objects.get(pk=build_id)
|
||||
location = (
|
||||
StockLocation.objects.filter(pk=location_id).first() if location_id else None
|
||||
)
|
||||
user = User.objects.filter(pk=user_id).first() if user_id else None
|
||||
|
||||
required_tests = build.part.getRequiredTests()
|
||||
|
||||
with transaction.atomic():
|
||||
for item in outputs:
|
||||
output = StockItem.objects.filter(pk=item['output_id']).first()
|
||||
if output:
|
||||
build.complete_build_output(
|
||||
output,
|
||||
user,
|
||||
quantity=item.get('quantity'),
|
||||
location=location,
|
||||
status=status,
|
||||
notes=notes,
|
||||
required_tests=required_tests,
|
||||
)
|
||||
|
||||
|
||||
@tracer.start_as_current_span('update_build_order_lines')
|
||||
def update_build_order_lines(bom_item_pk: int):
|
||||
"""Update all BuildOrderLineItem objects which reference a particular BomItem.
|
||||
|
||||
@@ -385,7 +385,7 @@ export function useCompleteBuildOutputsForm({
|
||||
title: t`Complete Build Outputs`,
|
||||
fields: buildOutputCompleteFields,
|
||||
onFormSuccess: onFormSuccess,
|
||||
successMessage: t`Build outputs have been completed`,
|
||||
successMessage: null,
|
||||
size: '80%'
|
||||
});
|
||||
}
|
||||
@@ -466,7 +466,7 @@ export function useScrapBuildOutputsForm({
|
||||
),
|
||||
fields: buildOutputScrapFields,
|
||||
onFormSuccess: onFormSuccess,
|
||||
successMessage: t`Build outputs have been scrapped`,
|
||||
successMessage: null,
|
||||
size: '80%'
|
||||
});
|
||||
}
|
||||
@@ -527,7 +527,7 @@ export function useCancelBuildOutputsForm({
|
||||
),
|
||||
fields: buildOutputCancelFields,
|
||||
onFormSuccess: onFormSuccess,
|
||||
successMessage: t`Build outputs have been cancelled`,
|
||||
successMessage: null,
|
||||
size: '80%'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -346,31 +346,62 @@ export default function BuildOutputTable({
|
||||
|
||||
const [selectedOutputs, setSelectedOutputs] = useState<any[]>([]);
|
||||
|
||||
const [completeTaskId, setCompleteTaskId] = useState<string>('');
|
||||
const [scrapTaskId, setScrapTaskId] = useState<string>('');
|
||||
const [deleteTaskId, setDeleteTaskId] = useState<string>('');
|
||||
|
||||
useBackgroundTask({
|
||||
taskId: completeTaskId,
|
||||
message: t`Completing build outputs`,
|
||||
successMessage: t`Build outputs have been completed`,
|
||||
onSuccess: () => {
|
||||
table.refreshTable(true);
|
||||
refreshBuild();
|
||||
}
|
||||
});
|
||||
|
||||
useBackgroundTask({
|
||||
taskId: scrapTaskId,
|
||||
message: t`Scrapping build outputs`,
|
||||
successMessage: t`Build outputs have been scrapped`,
|
||||
onSuccess: () => {
|
||||
table.refreshTable(true);
|
||||
refreshBuild();
|
||||
}
|
||||
});
|
||||
|
||||
useBackgroundTask({
|
||||
taskId: deleteTaskId,
|
||||
message: t`Cancelling build outputs`,
|
||||
successMessage: t`Build outputs have been cancelled`,
|
||||
onSuccess: () => {
|
||||
table.refreshTable(true);
|
||||
refreshBuild();
|
||||
}
|
||||
});
|
||||
|
||||
const completeBuildOutputsForm = useCompleteBuildOutputsForm({
|
||||
build: build,
|
||||
outputs: selectedOutputs,
|
||||
hasTrackedItems: hasTrackedItems,
|
||||
onFormSuccess: () => {
|
||||
table.refreshTable(true);
|
||||
refreshBuild();
|
||||
onFormSuccess: (response: any) => {
|
||||
setCompleteTaskId(response.task_id);
|
||||
}
|
||||
});
|
||||
|
||||
const scrapBuildOutputsForm = useScrapBuildOutputsForm({
|
||||
build: build,
|
||||
outputs: selectedOutputs,
|
||||
onFormSuccess: () => {
|
||||
table.refreshTable(true);
|
||||
refreshBuild();
|
||||
onFormSuccess: (response: any) => {
|
||||
setScrapTaskId(response.task_id);
|
||||
}
|
||||
});
|
||||
|
||||
const cancelBuildOutputsForm = useCancelBuildOutputsForm({
|
||||
build: build,
|
||||
outputs: selectedOutputs,
|
||||
onFormSuccess: () => {
|
||||
table.refreshTable(true);
|
||||
refreshBuild();
|
||||
onFormSuccess: (response: any) => {
|
||||
setDeleteTaskId(response.task_id);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user