mirror of
https://github.com/inventree/InvenTree.git
synced 2026-02-25 16:17:58 +00:00
Auto allocate tracked (#10887)
* Add "item_type" to BuildAutoAllocationSerializer * Update frontend to allow selection * Stub for allocating tracked items * Code for auto-allocating tracked outputs * Refactor auto-allocation code * UI updates * Bump API version * Auto refresh tracked items * Update CHANGELOG.md * docs entry * Add unit test * Add playwright testing
This commit is contained in:
@@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
[#11383](https://github.com/inventree/InvenTree/pull/11383) adds "exists_for_model_id", "exists_for_related_model", and "exists_for_related_model_id" filters to the ParameterTemplate API endpoint. These filters allow users to check for the existence of parameters associated with specific models or related models, improving the flexibility and usability of the API.
|
||||
|
||||
[#10887](https://github.com/inventree/InvenTree/pull/10887) adds the ability to auto-allocate tracked items against specific build outputs. Currently, this will only allocate items where the serial number of the tracked item matches the serial number of the build output, but in future this may be extended to allow for more flexible allocation rules.
|
||||
|
||||
### Changed
|
||||
|
||||
### Removed
|
||||
|
||||
@@ -108,9 +108,9 @@ Set this option to *True* to allow substitute parts (as specified by the BOM) to
|
||||
|
||||
Allocation of tracked stock items is slightly more complex. Instead of being allocated against the *Build Order*, tracked stock items must be allocated against an individual *Build Output*.
|
||||
|
||||
Allocating tracked stock items to particular build outputs is performed in the *Pending Items* tab:
|
||||
Allocating tracked stock items to particular build outputs is performed in the *Incomplete Outputs* tab:
|
||||
|
||||
In the *Pending Items* tab, we can see that each build output has a stock allocation requirement which must be met before that build output can be completed:
|
||||
In the *Incomplete Outputs* tab, we can see that each build output has a stock allocation requirement which must be met before that build output can be completed:
|
||||
|
||||
{{ image("build/build_allocate_tracked_parts.png", "Allocate tracked parts") }}
|
||||
|
||||
@@ -126,6 +126,12 @@ Here we can see that the incomplete build outputs (serial numbers 15 and 14) now
|
||||
!!! note "Example: Tracked Stock"
|
||||
Let's say we have 5 units of "Tracked Part" in stock - with 1 unit allocated to the build output. Once we complete the build output, there will be 4 units of "Tracked Part" in stock, with 1 unit being marked as "installed" within the assembled part
|
||||
|
||||
### Automatic Stock Allocation
|
||||
|
||||
Tracked stock items can be automatically allocated to build outputs using the *Auto Allocate* button in the *Incomplete Outputs* tab. This will attempt to allocate tracked stock items to build outputs based on matching serial numbers.
|
||||
|
||||
For each build output, the auto-allocation routine will attempt to find a matching component item with the same serial number. If such a stock item is found, and it is available for use, it will be allocated to that build output.
|
||||
|
||||
## Consuming Stock
|
||||
|
||||
Allocating stock items to a build order does not immediately remove them from stock. Instead, the stock items are marked as "allocated" against the build order, and are only removed from stock when they are "consumed" by the build order.
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 456
|
||||
INVENTREE_API_VERSION = 457
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v457 -> 2026-02-11 : https://github.com/inventree/InvenTree/pull/10887
|
||||
- Extend the "auto allocate" wizard API to include tracked items
|
||||
|
||||
v456 -> 2026-02-20 : https://github.com/inventree/InvenTree/pull/11303
|
||||
- Adds "primary" field to the SupplierPart API
|
||||
- Removes "default_supplier" field from the Part API
|
||||
|
||||
@@ -42,6 +42,7 @@ from common.settings import (
|
||||
get_global_setting,
|
||||
prevent_build_output_complete_on_incompleted_tests,
|
||||
)
|
||||
from generic.enums import StringEnum
|
||||
from generic.states import StateTransitionMixin, StatusCodeMixin
|
||||
from plugin.events import trigger_event
|
||||
from stock.status_codes import StockHistoryCode, StockStatus
|
||||
@@ -124,6 +125,13 @@ class Build(
|
||||
|
||||
order_insertion_by = ['reference']
|
||||
|
||||
class BuildItemTypes(StringEnum):
|
||||
"""Enumeration of available item types."""
|
||||
|
||||
ALL = 'all' # All BOM items (both tracked and untracked)
|
||||
TRACKED = 'tracked' # Tracked BOM items
|
||||
UNTRACKED = 'untracked' # Untracked BOM items
|
||||
|
||||
OVERDUE_FILTER = (
|
||||
Q(status__in=BuildStatusGroups.ACTIVE_CODES)
|
||||
& ~Q(target_date=None)
|
||||
@@ -950,49 +958,10 @@ class Build(
|
||||
|
||||
# Auto-allocate stock based on serial number
|
||||
if auto_allocate:
|
||||
for bom_item in trackable_parts:
|
||||
valid_part_ids = valid_parts.get(bom_item.pk, [])
|
||||
|
||||
# Find all matching stock items, based on serial number
|
||||
stock_items = list(
|
||||
stock.models.StockItem.objects.filter(
|
||||
part__pk__in=valid_part_ids,
|
||||
serial=output.serial,
|
||||
quantity=1,
|
||||
)
|
||||
)
|
||||
|
||||
# Filter stock items to only those which are in stock
|
||||
# Note that we can accept "in production" items here
|
||||
available_items = list(
|
||||
filter(
|
||||
lambda item: item.is_in_stock(
|
||||
check_in_production=False
|
||||
),
|
||||
stock_items,
|
||||
)
|
||||
)
|
||||
|
||||
if len(available_items) == 1:
|
||||
stock_item = available_items[0]
|
||||
|
||||
# Find the 'BuildLine' object which points to this BomItem
|
||||
try:
|
||||
build_line = BuildLine.objects.get(
|
||||
build=self, bom_item=bom_item
|
||||
)
|
||||
|
||||
# Allocate the stock items against the BuildLine
|
||||
allocations.append(
|
||||
BuildItem(
|
||||
build_line=build_line,
|
||||
stock_item=stock_item,
|
||||
quantity=1,
|
||||
install_into=output,
|
||||
)
|
||||
)
|
||||
except BuildLine.DoesNotExist:
|
||||
pass
|
||||
if new_allocations := self.auto_allocate_tracked_output(
|
||||
output, location=self.take_from
|
||||
):
|
||||
allocations.extend(new_allocations)
|
||||
|
||||
# Bulk create tracking entries
|
||||
stock.models.StockItemTracking.objects.bulk_create(tracking)
|
||||
@@ -1290,11 +1259,134 @@ class Build(
|
||||
self.save()
|
||||
|
||||
@transaction.atomic
|
||||
def auto_allocate_stock(self, **kwargs):
|
||||
def auto_allocate_stock(
|
||||
self, item_type: str = BuildItemTypes.UNTRACKED, **kwargs
|
||||
) -> None:
|
||||
"""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)
|
||||
Arguments:
|
||||
item_type: The type of BuildItem to allocate (default = untracked)
|
||||
"""
|
||||
if item_type in [self.BuildItemTypes.UNTRACKED, self.BuildItemTypes.ALL]:
|
||||
self.auto_allocate_untracked_stock(**kwargs)
|
||||
|
||||
if item_type in [self.BuildItemTypes.TRACKED, self.BuildItemTypes.ALL]:
|
||||
self.auto_allocate_tracked_stock(**kwargs)
|
||||
|
||||
def auto_allocate_tracked_output(self, output, **kwargs):
|
||||
"""Auto-allocate tracked stock items against a particular build output.
|
||||
|
||||
This may occur at the time of build output creation, or later when triggered manually.
|
||||
"""
|
||||
location = kwargs.get('location')
|
||||
exclude_location = kwargs.get('exclude_location')
|
||||
substitutes = kwargs.get('substitutes', True)
|
||||
optional_items = kwargs.get('optional_items', False)
|
||||
|
||||
# Newly created allocations (not yet committed to the database)
|
||||
allocations = []
|
||||
|
||||
# Return early if the output should not be auto-allocated
|
||||
if not output.serialized:
|
||||
return allocations
|
||||
|
||||
tracked_line_items = self.tracked_line_items.filter(
|
||||
bom_item__consumable=False, bom_item__sub_part__virtual=False
|
||||
)
|
||||
|
||||
for line_item in tracked_line_items:
|
||||
bom_item = line_item.bom_item
|
||||
|
||||
if bom_item.consumable:
|
||||
# Do not auto-allocate stock to consumable BOM items
|
||||
continue
|
||||
|
||||
if bom_item.optional and not optional_items:
|
||||
# User has specified that optional_items are to be ignored
|
||||
continue
|
||||
|
||||
# If the line item is already fully allocated, we can continue
|
||||
if line_item.is_fully_allocated():
|
||||
continue
|
||||
|
||||
# If there is already allocated stock against this build output, skip it
|
||||
if line_item.allocated_quantity(output=output) > 0:
|
||||
continue
|
||||
|
||||
# Find available parts (may include variants and substitutes)
|
||||
available_parts = bom_item.get_valid_parts_for_allocation(
|
||||
allow_variants=True, allow_substitutes=substitutes
|
||||
)
|
||||
|
||||
# Find stock items which match the output serial number
|
||||
available_stock = stock.models.StockItem.objects.filter(
|
||||
part__in=list(available_parts),
|
||||
part__active=True,
|
||||
part__virtual=False,
|
||||
serial=output.serial,
|
||||
).exclude(Q(serial=None) | Q(serial=''))
|
||||
|
||||
if location:
|
||||
# Filter only stock items located "below" the specified location
|
||||
sublocations = location.get_descendants(include_self=True)
|
||||
available_stock = available_stock.filter(
|
||||
location__in=list(sublocations)
|
||||
)
|
||||
|
||||
if exclude_location:
|
||||
# Exclude any stock items from the provided location
|
||||
sublocations = exclude_location.get_descendants(include_self=True)
|
||||
available_stock = available_stock.exclude(
|
||||
location__in=list(sublocations)
|
||||
)
|
||||
|
||||
# Filter stock items to only those which are in stock
|
||||
# Note that we can accept "in production" items here
|
||||
available_items = list(
|
||||
filter(
|
||||
lambda item: item.is_in_stock(check_in_production=False),
|
||||
available_stock,
|
||||
)
|
||||
)
|
||||
|
||||
if len(available_items) == 1:
|
||||
allocations.append(
|
||||
BuildItem(
|
||||
build_line=line_item,
|
||||
stock_item=available_items[0],
|
||||
quantity=1,
|
||||
install_into=output,
|
||||
)
|
||||
)
|
||||
|
||||
return allocations
|
||||
|
||||
def auto_allocate_tracked_stock(self, **kwargs):
|
||||
"""Automatically allocate tracked stock items against serialized build outputs.
|
||||
|
||||
This function allocates tracked stock items automatically against serialized build outputs,
|
||||
following a set of "guidelines":
|
||||
|
||||
- Only "tracked" BOM items are considered (untracked BOM items must be allocated separately)
|
||||
- Only build outputs with serial numbers are considered
|
||||
- Unallocated tracked components are allocated against build outputs with matching serial numbers
|
||||
"""
|
||||
new_items = []
|
||||
|
||||
# Select only "tracked" line items
|
||||
for output in self.incomplete_outputs.all():
|
||||
new_items.extend(self.auto_allocate_tracked_output(output, **kwargs))
|
||||
|
||||
# Bulk-create the new BuildItem objects
|
||||
BuildItem.objects.bulk_create(new_items)
|
||||
|
||||
def auto_allocate_untracked_stock(self, **kwargs):
|
||||
"""Automatically allocate untracked stock items against this build order.
|
||||
|
||||
This function allocates untracked stock items automatically against a BuildOrder,
|
||||
following a set of "guidelines":
|
||||
|
||||
- Only "untracked" BOM items are considered (tracked BOM items must be allocated separately)
|
||||
- If a particular BOM item is already fully allocated, it is skipped
|
||||
- Extract all available stock items for the BOM part
|
||||
- If variant stock is allowed, extract stock for those too
|
||||
@@ -1318,7 +1410,7 @@ class Build(
|
||||
|
||||
new_items = []
|
||||
|
||||
# Auto-allocation is only possible for "untracked" line items
|
||||
# Select only "untracked" line items
|
||||
for line_item in self.untracked_line_items.all():
|
||||
# Find the referenced BomItem
|
||||
bom_item = line_item.bom_item
|
||||
@@ -1352,6 +1444,11 @@ class Build(
|
||||
# Filter by list of available parts
|
||||
available_stock = available_stock.filter(part__in=list(available_parts))
|
||||
|
||||
# Ensure part is active and not virtual
|
||||
available_stock = available_stock.filter(
|
||||
part__active=True, part__virtual=False
|
||||
)
|
||||
|
||||
# Filter out "serialized" stock items, these cannot be auto-allocated
|
||||
available_stock = available_stock.filter(
|
||||
Q(serial=None) | Q(serial='')
|
||||
@@ -1691,11 +1788,14 @@ class BuildLine(report.mixins.InvenTreeReportMixin, InvenTree.models.InvenTreeMo
|
||||
"""Return the sub_part reference from the link bom_item."""
|
||||
return self.bom_item.sub_part
|
||||
|
||||
def allocated_quantity(self):
|
||||
def allocated_quantity(self, output: Optional[stock.models.StockItem] = None):
|
||||
"""Calculate the total allocated quantity for this BuildLine."""
|
||||
# Queryset containing all BuildItem objects allocated against this BuildLine
|
||||
allocations = self.allocations.all()
|
||||
|
||||
if output is not None:
|
||||
allocations = allocations.filter(install_into=output)
|
||||
|
||||
allocated = allocations.aggregate(
|
||||
q=Coalesce(Sum('quantity'), 0, output_field=models.DecimalField())
|
||||
)
|
||||
|
||||
@@ -1117,6 +1117,17 @@ class BuildAutoAllocationSerializer(serializers.Serializer):
|
||||
help_text=_('Allocate optional BOM items to build order'),
|
||||
)
|
||||
|
||||
item_type = serializers.ChoiceField(
|
||||
default=Build.BuildItemTypes.UNTRACKED,
|
||||
choices=[
|
||||
(Build.BuildItemTypes.ALL, _('All Items')),
|
||||
(Build.BuildItemTypes.UNTRACKED, _('Untracked Items')),
|
||||
(Build.BuildItemTypes.TRACKED, _('Tracked Items')),
|
||||
],
|
||||
label=_('Item Type'),
|
||||
help_text=_('Select item type to auto-allocate'),
|
||||
)
|
||||
|
||||
def save(self):
|
||||
"""Perform the auto-allocation step."""
|
||||
import InvenTree.tasks
|
||||
@@ -1133,6 +1144,7 @@ class BuildAutoAllocationSerializer(serializers.Serializer):
|
||||
interchangeable=data['interchangeable'],
|
||||
substitutes=data['substitutes'],
|
||||
optional_items=data['optional_items'],
|
||||
item_type=data.get('item_type', 'untracked'),
|
||||
group='build',
|
||||
):
|
||||
raise ValidationError(_('Failed to start auto-allocation task'))
|
||||
|
||||
@@ -920,6 +920,77 @@ class BuildAllocationTest(BuildAPITest):
|
||||
self.assertIsNotNone(bi)
|
||||
self.assertEqual(bi.build, build)
|
||||
|
||||
def test_auto_allocate_tracked(self):
|
||||
"""Test manual auto-allocation of tracked items against a Build."""
|
||||
# Create a base assembly
|
||||
assembly = Part.objects.create(
|
||||
name='Test Assembly',
|
||||
description='Test Assembly Description',
|
||||
assembly=True,
|
||||
trackable=True,
|
||||
)
|
||||
|
||||
component = Part.objects.create(
|
||||
name='Test Component',
|
||||
description='Test Component Description',
|
||||
trackable=True,
|
||||
component=True,
|
||||
)
|
||||
|
||||
# Create a BOM item for the assembly
|
||||
BomItem.objects.create(part=assembly, sub_part=component, quantity=1)
|
||||
|
||||
# Create a build order for the assembly
|
||||
build = Build.objects.create(part=assembly, reference='BO-12347', quantity=10)
|
||||
|
||||
SN = '123456'
|
||||
|
||||
# Create serialized component item
|
||||
c = StockItem.objects.create(part=component, quantity=1, serial=SN)
|
||||
|
||||
N = BuildItem.objects.count()
|
||||
|
||||
# Create a new build output
|
||||
response = self.post(
|
||||
reverse('api-build-output-create', kwargs={'pk': build.pk}),
|
||||
{'quantity': 1, 'serial_numbers': SN, 'auto_allocate': False},
|
||||
expected_code=201,
|
||||
)
|
||||
|
||||
output = response.data[0]
|
||||
|
||||
self.assertIsNotNone(output)
|
||||
self.assertIsNotNone(output['pk'])
|
||||
self.assertEqual(output['serial'], SN)
|
||||
|
||||
# No new build items (allocations) have been created yet
|
||||
self.assertEqual(N, BuildItem.objects.count())
|
||||
|
||||
# Let's auto-allocate via the API now
|
||||
url = reverse('api-build-auto-allocate', kwargs={'pk': build.pk})
|
||||
|
||||
# Allocate only 'untracked' items - this should not allocate our tracked item
|
||||
self.post(url, data={'item_type': 'untracked'})
|
||||
|
||||
self.assertEqual(N, BuildItem.objects.count())
|
||||
|
||||
# Allocate 'tracked' items - this should allocate our tracked item
|
||||
self.post(url, data={'item_type': 'tracked'})
|
||||
|
||||
# A new BuildItem should have been created
|
||||
self.assertEqual(N + 1, BuildItem.objects.count())
|
||||
|
||||
line = build.build_lines.first()
|
||||
|
||||
self.assertIsNotNone(line)
|
||||
allocations = line.allocations.filter(install_into_id=output['pk'])
|
||||
self.assertEqual(allocations.count(), 1)
|
||||
|
||||
allocation = allocations.first()
|
||||
|
||||
self.assertEqual(allocation.stock_item, c)
|
||||
self.assertEqual(allocation.quantity, 1)
|
||||
|
||||
|
||||
class BuildItemTest(BuildAPITest):
|
||||
"""Unit tests for build items.
|
||||
|
||||
@@ -226,6 +226,31 @@ export function useBuildOrderOutputFields({
|
||||
}, [quantity, batchGenerator.result, serialGenerator.result, trackable]);
|
||||
}
|
||||
|
||||
export function useBuildAutoAllocateFields({
|
||||
item_type
|
||||
}: {
|
||||
item_type: 'all' | 'tracked' | 'untracked';
|
||||
}): ApiFormFieldSet {
|
||||
return useMemo(() => {
|
||||
return {
|
||||
location: {},
|
||||
exclude_location: {},
|
||||
item_type: {
|
||||
value: item_type,
|
||||
hidden: true
|
||||
},
|
||||
interchangeable: {
|
||||
hidden: item_type === 'tracked'
|
||||
},
|
||||
substitutes: {},
|
||||
optional_items: {
|
||||
hidden: item_type === 'tracked',
|
||||
value: item_type === 'tracked' ? false : undefined
|
||||
}
|
||||
};
|
||||
}, [item_type]);
|
||||
}
|
||||
|
||||
function BuildOutputFormRow({
|
||||
props,
|
||||
record,
|
||||
|
||||
@@ -27,6 +27,7 @@ import type { RowAction, TableColumn } from '@lib/types/Tables';
|
||||
import OrderPartsWizard from '../../components/wizards/OrderPartsWizard';
|
||||
import {
|
||||
useAllocateStockToBuildForm,
|
||||
useBuildAutoAllocateFields,
|
||||
useBuildOrderFields,
|
||||
useConsumeBuildLinesForm
|
||||
} from '../../forms/BuildForms';
|
||||
@@ -574,17 +575,9 @@ export default function BuildLineTable({
|
||||
url: ApiEndpoints.build_order_auto_allocate,
|
||||
pk: build.pk,
|
||||
title: t`Allocate Stock`,
|
||||
fields: {
|
||||
location: {
|
||||
filters: {
|
||||
structural: false
|
||||
}
|
||||
},
|
||||
exclude_location: {},
|
||||
interchangeable: {},
|
||||
substitutes: {},
|
||||
optional_items: {}
|
||||
},
|
||||
fields: useBuildAutoAllocateFields({
|
||||
item_type: 'untracked'
|
||||
}),
|
||||
initialData: {
|
||||
location: build.take_from,
|
||||
interchangeable: true,
|
||||
@@ -595,7 +588,7 @@ export default function BuildLineTable({
|
||||
table: table,
|
||||
preFormContent: (
|
||||
<Alert color='green' title={t`Auto Allocate Stock`}>
|
||||
<Text>{t`Automatically allocate stock to this build according to the selected options`}</Text>
|
||||
<Text>{t`Automatically allocate untracked BOM items to this build according to the selected options`}</Text>
|
||||
</Alert>
|
||||
)
|
||||
});
|
||||
|
||||
@@ -14,7 +14,8 @@ import {
|
||||
IconBuildingFactory2,
|
||||
IconCircleCheck,
|
||||
IconCircleX,
|
||||
IconExclamationCircle
|
||||
IconExclamationCircle,
|
||||
IconWand
|
||||
} from '@tabler/icons-react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
@@ -33,6 +34,7 @@ import type { TableColumn } from '@lib/types/Tables';
|
||||
import { StylishText } from '../../components/items/StylishText';
|
||||
import { useApi } from '../../contexts/ApiContext';
|
||||
import {
|
||||
useBuildAutoAllocateFields,
|
||||
useBuildOrderOutputFields,
|
||||
useCancelBuildOutputsForm,
|
||||
useCompleteBuildOutputsForm,
|
||||
@@ -213,6 +215,32 @@ export default function BuildOutputTable({
|
||||
}
|
||||
});
|
||||
|
||||
const autoAllocateStock = useCreateApiFormModal({
|
||||
url: ApiEndpoints.build_order_auto_allocate,
|
||||
pk: build.pk,
|
||||
title: t`Allocate Stock`,
|
||||
fields: useBuildAutoAllocateFields({
|
||||
item_type: 'tracked'
|
||||
}),
|
||||
initialData: {
|
||||
location: build.take_from,
|
||||
substitutes: true
|
||||
},
|
||||
successMessage: t`Auto-allocation in progress`,
|
||||
onFormSuccess: () => {
|
||||
// After a short delay, refresh the tracked items
|
||||
setTimeout(() => {
|
||||
refetchTrackedItems();
|
||||
}, 2500);
|
||||
},
|
||||
table: table,
|
||||
preFormContent: (
|
||||
<Alert color='green' title={t`Auto Allocate Stock`}>
|
||||
<Text>{t`Automatically allocate tracked BOM items to this build according to the selected options`}</Text>
|
||||
</Alert>
|
||||
)
|
||||
});
|
||||
|
||||
const hasTrackedItems: boolean = useMemo(() => {
|
||||
return (trackedItems?.length ?? 0) > 0;
|
||||
}, [trackedItems]);
|
||||
@@ -438,6 +466,16 @@ export default function BuildOutputTable({
|
||||
const tableActions = useMemo(() => {
|
||||
return [
|
||||
stockAdjustActions.dropdown,
|
||||
<ActionButton
|
||||
key='allocate-stock'
|
||||
icon={<IconWand />}
|
||||
color='blue'
|
||||
tooltip={t`Auto Allocate Stock`}
|
||||
hidden={!hasTrackedItems}
|
||||
onClick={() => {
|
||||
autoAllocateStock.open();
|
||||
}}
|
||||
/>,
|
||||
<ActionButton
|
||||
key='complete-selected-outputs'
|
||||
tooltip={t`Complete selected outputs`}
|
||||
@@ -480,6 +518,7 @@ export default function BuildOutputTable({
|
||||
];
|
||||
}, [
|
||||
build,
|
||||
hasTrackedItems,
|
||||
user,
|
||||
table.selectedRecords,
|
||||
table.hasSelectedRecords,
|
||||
@@ -680,6 +719,7 @@ export default function BuildOutputTable({
|
||||
return (
|
||||
<>
|
||||
{addBuildOutput.modal}
|
||||
{autoAllocateStock.modal}
|
||||
{completeBuildOutputsForm.modal}
|
||||
{scrapBuildOutputsForm.modal}
|
||||
{editBuildOutput.modal}
|
||||
|
||||
@@ -431,6 +431,40 @@ test('Build Order - Allocation', async ({ browser }) => {
|
||||
.waitFor();
|
||||
});
|
||||
|
||||
test('Build Order - Auto Allocate Tracked', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser, {
|
||||
url: 'manufacturing/build-order/27/consumed-stock'
|
||||
});
|
||||
|
||||
await loadTab(page, 'Incomplete Outputs');
|
||||
|
||||
await page.getByRole('cell', { name: '0 / 6' }).waitFor();
|
||||
|
||||
// Auto-allocate tracked stock
|
||||
await page
|
||||
.getByRole('button', { name: 'action-button-auto-allocate-' })
|
||||
.click();
|
||||
|
||||
// Wait for auto-filled form field
|
||||
await page
|
||||
.locator('div')
|
||||
.filter({ hasText: /^Factory$/ })
|
||||
.first()
|
||||
.waitFor();
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
// Wait for one of the required parts to be allocated
|
||||
await page.getByRole('cell', { name: '1 / 6' }).waitFor({ timeout: 7500 });
|
||||
|
||||
// Deallocate the item to return to the initial state
|
||||
const cell = await page.getByRole('cell', { name: '# 555' });
|
||||
await clickOnRowMenu(cell);
|
||||
await page.getByRole('menuitem', { name: 'Deallocate' }).click();
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
await page.getByRole('cell', { name: '0 / 6' }).waitFor({ timeout: 7500 });
|
||||
});
|
||||
|
||||
// Test partial stock consumption against build order
|
||||
test('Build Order - Consume Stock', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser, {
|
||||
|
||||
Reference in New Issue
Block a user