mirror of
https://github.com/inventree/InvenTree.git
synced 2026-05-28 11:59:23 +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():
|
def isInTestMode():
|
||||||
"""Returns True if the database is in testing mode."""
|
"""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():
|
def isWaitingForDatabase():
|
||||||
|
|||||||
@@ -735,14 +735,81 @@ class BuildOutputScrap(BuildOrderContextMixin, CreateAPI):
|
|||||||
ctx['to_complete'] = False
|
ctx['to_complete'] = False
|
||||||
return ctx
|
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):
|
class BuildOutputComplete(BuildOrderContextMixin, CreateAPI):
|
||||||
"""API endpoint for completing build outputs."""
|
"""API endpoint for completing build outputs."""
|
||||||
|
|
||||||
queryset = Build.objects.none()
|
queryset = Build.objects.none()
|
||||||
|
|
||||||
serializer_class = build.serializers.BuildOutputCompleteSerializer
|
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):
|
class BuildOutputDelete(BuildOrderContextMixin, CreateAPI):
|
||||||
"""API endpoint for deleting multiple build outputs."""
|
"""API endpoint for deleting multiple build outputs."""
|
||||||
@@ -750,15 +817,33 @@ class BuildOutputDelete(BuildOrderContextMixin, CreateAPI):
|
|||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
"""Add extra context information to the endpoint serializer."""
|
"""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
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
queryset = Build.objects.none()
|
queryset = Build.objects.none()
|
||||||
|
|
||||||
serializer_class = build.serializers.BuildOutputDeleteSerializer
|
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):
|
class BuildFinish(BuildOrderContextMixin, CreateAPI):
|
||||||
"""API endpoint for marking a build as finished (completed)."""
|
"""API endpoint for marking a build as finished (completed)."""
|
||||||
|
|||||||
@@ -462,18 +462,6 @@ class BuildOutputDeleteSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
return data
|
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):
|
class BuildOutputScrapSerializer(serializers.Serializer):
|
||||||
"""Scrapping one or more build outputs."""
|
"""Scrapping one or more build outputs."""
|
||||||
@@ -518,27 +506,6 @@ class BuildOutputScrapSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
return data
|
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):
|
class BuildOutputCompleteSerializer(serializers.Serializer):
|
||||||
"""DRF serializer for completing one or more build outputs."""
|
"""DRF serializer for completing one or more build outputs."""
|
||||||
@@ -610,42 +577,6 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
return data
|
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):
|
class BuildIssueSerializer(serializers.Serializer):
|
||||||
"""DRF serializer for issuing a build order."""
|
"""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)
|
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')
|
@tracer.start_as_current_span('update_build_order_lines')
|
||||||
def update_build_order_lines(bom_item_pk: int):
|
def update_build_order_lines(bom_item_pk: int):
|
||||||
"""Update all BuildOrderLineItem objects which reference a particular BomItem.
|
"""Update all BuildOrderLineItem objects which reference a particular BomItem.
|
||||||
|
|||||||
@@ -385,7 +385,7 @@ export function useCompleteBuildOutputsForm({
|
|||||||
title: t`Complete Build Outputs`,
|
title: t`Complete Build Outputs`,
|
||||||
fields: buildOutputCompleteFields,
|
fields: buildOutputCompleteFields,
|
||||||
onFormSuccess: onFormSuccess,
|
onFormSuccess: onFormSuccess,
|
||||||
successMessage: t`Build outputs have been completed`,
|
successMessage: null,
|
||||||
size: '80%'
|
size: '80%'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -466,7 +466,7 @@ export function useScrapBuildOutputsForm({
|
|||||||
),
|
),
|
||||||
fields: buildOutputScrapFields,
|
fields: buildOutputScrapFields,
|
||||||
onFormSuccess: onFormSuccess,
|
onFormSuccess: onFormSuccess,
|
||||||
successMessage: t`Build outputs have been scrapped`,
|
successMessage: null,
|
||||||
size: '80%'
|
size: '80%'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -527,7 +527,7 @@ export function useCancelBuildOutputsForm({
|
|||||||
),
|
),
|
||||||
fields: buildOutputCancelFields,
|
fields: buildOutputCancelFields,
|
||||||
onFormSuccess: onFormSuccess,
|
onFormSuccess: onFormSuccess,
|
||||||
successMessage: t`Build outputs have been cancelled`,
|
successMessage: null,
|
||||||
size: '80%'
|
size: '80%'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -346,31 +346,62 @@ export default function BuildOutputTable({
|
|||||||
|
|
||||||
const [selectedOutputs, setSelectedOutputs] = useState<any[]>([]);
|
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({
|
const completeBuildOutputsForm = useCompleteBuildOutputsForm({
|
||||||
build: build,
|
build: build,
|
||||||
outputs: selectedOutputs,
|
outputs: selectedOutputs,
|
||||||
hasTrackedItems: hasTrackedItems,
|
hasTrackedItems: hasTrackedItems,
|
||||||
onFormSuccess: () => {
|
onFormSuccess: (response: any) => {
|
||||||
table.refreshTable(true);
|
setCompleteTaskId(response.task_id);
|
||||||
refreshBuild();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const scrapBuildOutputsForm = useScrapBuildOutputsForm({
|
const scrapBuildOutputsForm = useScrapBuildOutputsForm({
|
||||||
build: build,
|
build: build,
|
||||||
outputs: selectedOutputs,
|
outputs: selectedOutputs,
|
||||||
onFormSuccess: () => {
|
onFormSuccess: (response: any) => {
|
||||||
table.refreshTable(true);
|
setScrapTaskId(response.task_id);
|
||||||
refreshBuild();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const cancelBuildOutputsForm = useCancelBuildOutputsForm({
|
const cancelBuildOutputsForm = useCancelBuildOutputsForm({
|
||||||
build: build,
|
build: build,
|
||||||
outputs: selectedOutputs,
|
outputs: selectedOutputs,
|
||||||
onFormSuccess: () => {
|
onFormSuccess: (response: any) => {
|
||||||
table.refreshTable(true);
|
setDeleteTaskId(response.task_id);
|
||||||
refreshBuild();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user