2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-29 12:06:44 +00:00

Stock Transfer Improvements (#8570)

* Allow transfer of items independent of status marker

* Update test

* Display errors in stock transsfer form

* Add option to set status when transferring stock

* Fix inStock check for stock actions

* Allow adjustment of status when counting stock item

* Allow status adjustment for other actions:

- Remove stock
- Add stock

* Revert error behavior

* Enhanced unit test

* Unit test fix

* Bump API version

* Fix for playwright test

- Added helper func

* Extend playwright tests for stock actions
This commit is contained in:
Oliver 2024-11-28 07:06:58 +11:00 committed by GitHub
parent 28ea275d1a
commit c074250ce6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 281 additions and 52 deletions

View File

@ -1,13 +1,16 @@
"""InvenTree API version information.""" """InvenTree API version information."""
# InvenTree API version # InvenTree API version
INVENTREE_API_VERSION = 288 INVENTREE_API_VERSION = 289
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" """Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """ INVENTREE_API_TEXT = """
v289 - 2024-11-27 : https://github.com/inventree/InvenTree/pull/8570
- Enable status change when transferring stock items
v288 - 2024-11-27 : https://github.com/inventree/InvenTree/pull/8574 v288 - 2024-11-27 : https://github.com/inventree/InvenTree/pull/8574
- Adds "consumed" filter to StockItem API - Adds "consumed" filter to StockItem API

View File

@ -1489,12 +1489,15 @@ class StockItem(
""" """
return self.children.count() return self.children.count()
@property def is_in_stock(self, check_status: bool = True):
def in_stock(self) -> bool: """Return True if this StockItem is "in stock".
"""Returns True if this item is in stock.
See also: StockItem.IN_STOCK_FILTER for the db optimized version of this check. Args:
check_status: If True, check the status of the StockItem. Defaults to True.
""" """
if check_status and self.status not in StockStatusGroups.AVAILABLE_CODES:
return False
return all([ return all([
self.quantity > 0, # Quantity must be greater than zero self.quantity > 0, # Quantity must be greater than zero
self.sales_order is None, # Not assigned to a SalesOrder self.sales_order is None, # Not assigned to a SalesOrder
@ -1502,9 +1505,16 @@ class StockItem(
self.customer is None, # Not assigned to a customer self.customer is None, # Not assigned to a customer
self.consumed_by is None, # Not consumed by a build self.consumed_by is None, # Not consumed by a build
not self.is_building, # Not part of an active build not self.is_building, # Not part of an active build
self.status in StockStatusGroups.AVAILABLE_CODES, # Status is "available"
]) ])
@property
def in_stock(self) -> bool:
"""Returns True if this item is in stock.
See also: StockItem.IN_STOCK_FILTER for the db optimized version of this check.
"""
return self.is_in_stock(check_status=True)
@property @property
def can_adjust_location(self): def can_adjust_location(self):
"""Returns True if the stock location can be "adjusted" for this part. """Returns True if the stock location can be "adjusted" for this part.
@ -2073,14 +2083,13 @@ class StockItem(
'STOCK_ALLOW_OUT_OF_STOCK_TRANSFER', backup_value=False, cache=False 'STOCK_ALLOW_OUT_OF_STOCK_TRANSFER', backup_value=False, cache=False
) )
if not allow_out_of_stock_transfer and not self.in_stock: if not allow_out_of_stock_transfer and not self.is_in_stock(check_status=False):
raise ValidationError(_('StockItem cannot be moved as it is not in stock')) raise ValidationError(_('StockItem cannot be moved as it is not in stock'))
if quantity <= 0: if quantity <= 0:
return False return False
if location is None: if location is None:
# TODO - Raise appropriate error (cannot move to blank location)
return False return False
# Test for a partial movement # Test for a partial movement
@ -2161,11 +2170,16 @@ class StockItem(
return True return True
@transaction.atomic @transaction.atomic
def stocktake(self, count, user, notes=''): def stocktake(self, count, user, **kwargs):
"""Perform item stocktake. """Perform item stocktake.
When the quantity of an item is counted, Arguments:
record the date of stocktake count: The new quantity of the item
user: The user performing the stocktake
Keyword Arguments:
notes: Optional notes for the stocktake
status: Optionally adjust the stock status
""" """
try: try:
count = Decimal(count) count = Decimal(count)
@ -2175,25 +2189,40 @@ class StockItem(
if count < 0: if count < 0:
return False return False
if self.updateQuantity(count):
tracking_info = {'quantity': float(count)}
self.stocktake_date = InvenTree.helpers.current_date() self.stocktake_date = InvenTree.helpers.current_date()
self.stocktake_user = user self.stocktake_user = user
if self.updateQuantity(count): # Optional fields which can be supplied in a 'stocktake' call
for field in StockItem.optional_transfer_fields():
if field in kwargs:
setattr(self, field, kwargs[field])
tracking_info[field] = kwargs[field]
self.save(add_note=False)
self.add_tracking_entry( self.add_tracking_entry(
StockHistoryCode.STOCK_COUNT, StockHistoryCode.STOCK_COUNT,
user, user,
notes=notes, notes=kwargs.get('notes', ''),
deltas={'quantity': float(self.quantity)}, deltas=tracking_info,
) )
return True return True
@transaction.atomic @transaction.atomic
def add_stock(self, quantity, user, notes=''): def add_stock(self, quantity, user, **kwargs):
"""Add items to stock. """Add a specified quantity of stock to this item.
This function can be called by initiating a ProjectRun, Arguments:
or by manually adding the items to the stock location quantity: The quantity to add
user: The user performing the action
Keyword Arguments:
notes: Optional notes for the stock addition
status: Optionally adjust the stock status
""" """
# Cannot add items to a serialized part # Cannot add items to a serialized part
if self.serialized: if self.serialized:
@ -2209,20 +2238,38 @@ class StockItem(
return False return False
if self.updateQuantity(self.quantity + quantity): if self.updateQuantity(self.quantity + quantity):
tracking_info = {'added': float(quantity), 'quantity': float(self.quantity)}
# Optional fields which can be supplied in a 'stocktake' call
for field in StockItem.optional_transfer_fields():
if field in kwargs:
setattr(self, field, kwargs[field])
tracking_info[field] = kwargs[field]
self.save(add_note=False)
self.add_tracking_entry( self.add_tracking_entry(
StockHistoryCode.STOCK_ADD, StockHistoryCode.STOCK_ADD,
user, user,
notes=notes, notes=kwargs.get('notes', ''),
deltas={'added': float(quantity), 'quantity': float(self.quantity)}, deltas=tracking_info,
) )
return True return True
@transaction.atomic @transaction.atomic
def take_stock( def take_stock(self, quantity, user, code=StockHistoryCode.STOCK_REMOVE, **kwargs):
self, quantity, user, notes='', code=StockHistoryCode.STOCK_REMOVE, **kwargs """Remove the specified quantity from this StockItem.
):
"""Remove items from stock.""" Arguments:
quantity: The quantity to remove
user: The user performing the action
Keyword Arguments:
code: The stock history code to use
notes: Optional notes for the stock removal
status: Optionally adjust the stock status
"""
# Cannot remove items from a serialized part # Cannot remove items from a serialized part
if self.serialized: if self.serialized:
return False return False
@ -2244,7 +2291,17 @@ class StockItem(
if stockitem := kwargs.get('stockitem'): if stockitem := kwargs.get('stockitem'):
deltas['stockitem'] = stockitem.pk deltas['stockitem'] = stockitem.pk
self.add_tracking_entry(code, user, notes=notes, deltas=deltas) # Optional fields which can be supplied in a 'stocktake' call
for field in StockItem.optional_transfer_fields():
if field in kwargs:
setattr(self, field, kwargs[field])
deltas[field] = kwargs[field]
self.save(add_note=False)
self.add_tracking_entry(
code, user, notes=kwargs.get('notes', ''), deltas=deltas
)
return True return True

View File

@ -1554,7 +1554,7 @@ class StockAdjustmentItemSerializer(serializers.Serializer):
class Meta: class Meta:
"""Metaclass options.""" """Metaclass options."""
fields = ['item', 'quantity'] fields = ['pk', 'quantity', 'batch', 'status', 'packaging']
pk = serializers.PrimaryKeyRelatedField( pk = serializers.PrimaryKeyRelatedField(
queryset=StockItem.objects.all(), queryset=StockItem.objects.all(),
@ -1565,6 +1565,17 @@ class StockAdjustmentItemSerializer(serializers.Serializer):
help_text=_('StockItem primary key value'), help_text=_('StockItem primary key value'),
) )
def validate_pk(self, pk):
"""Ensure the stock item is valid."""
allow_out_of_stock_transfer = get_global_setting(
'STOCK_ALLOW_OUT_OF_STOCK_TRANSFER', backup_value=False, cache=False
)
if not allow_out_of_stock_transfer and not pk.is_in_stock(check_status=False):
raise ValidationError(_('Stock item is not in stock'))
return pk
quantity = serializers.DecimalField( quantity = serializers.DecimalField(
max_digits=15, decimal_places=5, min_value=Decimal(0), required=True max_digits=15, decimal_places=5, min_value=Decimal(0), required=True
) )
@ -1640,7 +1651,14 @@ class StockCountSerializer(StockAdjustmentSerializer):
stock_item = item['pk'] stock_item = item['pk']
quantity = item['quantity'] quantity = item['quantity']
stock_item.stocktake(quantity, request.user, notes=notes) # Optional fields
extra = {}
for field_name in StockItem.optional_transfer_fields():
if field_value := item.get(field_name, None):
extra[field_name] = field_value
stock_item.stocktake(quantity, request.user, notes=notes, **extra)
class StockAddSerializer(StockAdjustmentSerializer): class StockAddSerializer(StockAdjustmentSerializer):
@ -1658,7 +1676,14 @@ class StockAddSerializer(StockAdjustmentSerializer):
stock_item = item['pk'] stock_item = item['pk']
quantity = item['quantity'] quantity = item['quantity']
stock_item.add_stock(quantity, request.user, notes=notes) # Optional fields
extra = {}
for field_name in StockItem.optional_transfer_fields():
if field_value := item.get(field_name, None):
extra[field_name] = field_value
stock_item.add_stock(quantity, request.user, notes=notes, **extra)
class StockRemoveSerializer(StockAdjustmentSerializer): class StockRemoveSerializer(StockAdjustmentSerializer):
@ -1676,7 +1701,14 @@ class StockRemoveSerializer(StockAdjustmentSerializer):
stock_item = item['pk'] stock_item = item['pk']
quantity = item['quantity'] quantity = item['quantity']
stock_item.take_stock(quantity, request.user, notes=notes) # Optional fields
extra = {}
for field_name in StockItem.optional_transfer_fields():
if field_value := item.get(field_name, None):
extra[field_name] = field_value
stock_item.take_stock(quantity, request.user, notes=notes, **extra)
class StockTransferSerializer(StockAdjustmentSerializer): class StockTransferSerializer(StockAdjustmentSerializer):

View File

@ -1780,8 +1780,8 @@ class StocktakeTest(StockAPITestCase):
"""Test stock transfers.""" """Test stock transfers."""
stock_item = StockItem.objects.get(pk=1234) stock_item = StockItem.objects.get(pk=1234)
# Mark this stock item as "quarantined" (cannot be moved) # Mark the item as 'out of stock' by assigning a customer
stock_item.status = StockStatus.QUARANTINED.value stock_item.customer = company.models.Company.objects.first()
stock_item.save() stock_item.save()
InvenTreeSetting.set_setting('STOCK_ALLOW_OUT_OF_STOCK_TRANSFER', False) InvenTreeSetting.set_setting('STOCK_ALLOW_OUT_OF_STOCK_TRANSFER', False)
@ -1797,7 +1797,7 @@ class StocktakeTest(StockAPITestCase):
# First attempt should *fail* - stock item is quarantined # First attempt should *fail* - stock item is quarantined
response = self.post(url, data, expected_code=400) response = self.post(url, data, expected_code=400)
self.assertIn('cannot be moved as it is not in stock', str(response.data)) self.assertIn('Stock item is not in stock', str(response.data))
# Now, allow transfer of "out of stock" items # Now, allow transfer of "out of stock" items
InvenTreeSetting.set_setting('STOCK_ALLOW_OUT_OF_STOCK_TRANSFER', True) InvenTreeSetting.set_setting('STOCK_ALLOW_OUT_OF_STOCK_TRANSFER', True)

View File

@ -13,7 +13,7 @@ from company.models import Company
from InvenTree.unit_test import AdminTestCase, InvenTreeTestCase from InvenTree.unit_test import AdminTestCase, InvenTreeTestCase
from order.models import SalesOrder from order.models import SalesOrder
from part.models import Part, PartTestTemplate from part.models import Part, PartTestTemplate
from stock.status_codes import StockHistoryCode from stock.status_codes import StockHistoryCode, StockStatus
from .models import ( from .models import (
StockItem, StockItem,
@ -444,11 +444,32 @@ class StockTest(StockTestBase):
self.assertIn('Counted items', track.notes) self.assertIn('Counted items', track.notes)
n = it.tracking_info.count() n = it.tracking_info.count()
self.assertFalse(it.stocktake(-1, None, 'test negative stocktake')) self.assertFalse(
it.stocktake(
-1,
None,
notes='test negative stocktake',
status=StockStatus.DAMAGED.value,
)
)
# Ensure tracking info was not added # Ensure tracking info was not added
self.assertEqual(it.tracking_info.count(), n) self.assertEqual(it.tracking_info.count(), n)
it.refresh_from_db()
self.assertEqual(it.status, StockStatus.OK.value)
# Next, perform a valid stocktake
self.assertTrue(
it.stocktake(
100, None, notes='test stocktake', status=StockStatus.DAMAGED.value
)
)
it.refresh_from_db()
self.assertEqual(it.quantity, 100)
self.assertEqual(it.status, StockStatus.DAMAGED.value)
def test_add_stock(self): def test_add_stock(self):
"""Test adding stock.""" """Test adding stock."""
it = StockItem.objects.get(pk=2) it = StockItem.objects.get(pk=2)

View File

@ -23,6 +23,11 @@ export type ApiFormAdjustFilterType = {
data: FieldValues; data: FieldValues;
}; };
export type ApiFormFieldChoice = {
value: any;
display_name: string;
};
/** Definition of the ApiForm field component. /** Definition of the ApiForm field component.
* - The 'name' attribute *must* be provided * - The 'name' attribute *must* be provided
* - All other attributes are optional, and may be provided by the API * - All other attributes are optional, and may be provided by the API
@ -83,7 +88,7 @@ export type ApiFormFieldType = {
child?: ApiFormFieldType; child?: ApiFormFieldType;
children?: { [key: string]: ApiFormFieldType }; children?: { [key: string]: ApiFormFieldType };
required?: boolean; required?: boolean;
choices?: any[]; choices?: ApiFormFieldChoice[];
hidden?: boolean; hidden?: boolean;
disabled?: boolean; disabled?: boolean;
exclude?: boolean; exclude?: boolean;

View File

@ -213,6 +213,7 @@ export function TableField({
*/ */
export function TableFieldExtraRow({ export function TableFieldExtraRow({
visible, visible,
fieldName,
fieldDefinition, fieldDefinition,
defaultValue, defaultValue,
emptyValue, emptyValue,
@ -220,6 +221,7 @@ export function TableFieldExtraRow({
onValueChange onValueChange
}: { }: {
visible: boolean; visible: boolean;
fieldName?: string;
fieldDefinition: ApiFormFieldType; fieldDefinition: ApiFormFieldType;
defaultValue?: any; defaultValue?: any;
error?: string; error?: string;
@ -253,6 +255,7 @@ export function TableFieldExtraRow({
<InvenTreeIcon icon='downright' /> <InvenTreeIcon icon='downright' />
</Container> </Container>
<StandaloneField <StandaloneField
fieldName={fieldName ?? 'field'}
fieldDefinition={field} fieldDefinition={field}
defaultValue={defaultValue} defaultValue={defaultValue}
error={error} error={error}

View File

@ -1,5 +1,5 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Flex, Group, Skeleton, Table, Text } from '@mantine/core'; import { Flex, Group, Skeleton, Stack, Table, Text } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks'; import { useDisclosure } from '@mantine/hooks';
import { modals } from '@mantine/modals'; import { modals } from '@mantine/modals';
import { import {
@ -19,6 +19,7 @@ import RemoveRowButton from '../components/buttons/RemoveRowButton';
import { StandaloneField } from '../components/forms/StandaloneField'; import { StandaloneField } from '../components/forms/StandaloneField';
import type { import type {
ApiFormAdjustFilterType, ApiFormAdjustFilterType,
ApiFormFieldChoice,
ApiFormFieldSet ApiFormFieldSet
} from '../components/forms/fields/ApiFormField'; } from '../components/forms/fields/ApiFormField';
import { import {
@ -43,6 +44,7 @@ import {
import { useSerialNumberPlaceholder } from '../hooks/UsePlaceholder'; import { useSerialNumberPlaceholder } from '../hooks/UsePlaceholder';
import { apiUrl } from '../states/ApiState'; import { apiUrl } from '../states/ApiState';
import { useGlobalSettingsState } from '../states/SettingsState'; import { useGlobalSettingsState } from '../states/SettingsState';
import { StatusFilterOptions } from '../tables/Filter';
/** /**
* Construct a set of fields for creating / editing a StockItem instance * Construct a set of fields for creating / editing a StockItem instance
@ -430,6 +432,7 @@ type StockRow = {
function StockOperationsRow({ function StockOperationsRow({
props, props,
transfer = false, transfer = false,
changeStatus = false,
add = false, add = false,
setMax = false, setMax = false,
merge = false, merge = false,
@ -437,15 +440,29 @@ function StockOperationsRow({
}: { }: {
props: TableFieldRowProps; props: TableFieldRowProps;
transfer?: boolean; transfer?: boolean;
changeStatus?: boolean;
add?: boolean; add?: boolean;
setMax?: boolean; setMax?: boolean;
merge?: boolean; merge?: boolean;
record?: any; record?: any;
}) { }) {
const statusOptions: ApiFormFieldChoice[] = useMemo(() => {
return (
StatusFilterOptions(ModelType.stockitem)()?.map((choice) => {
return {
value: choice.value,
display_name: choice.label
};
}) ?? []
);
}, []);
const [quantity, setQuantity] = useState<StockItemQuantity>( const [quantity, setQuantity] = useState<StockItemQuantity>(
add ? 0 : (props.item?.quantity ?? 0) add ? 0 : (props.item?.quantity ?? 0)
); );
const [status, setStatus] = useState<number | undefined>(undefined);
const removeAndRefresh = () => { const removeAndRefresh = () => {
props.removeFn(props.idx); props.removeFn(props.idx);
}; };
@ -463,6 +480,17 @@ function StockOperationsRow({
} }
}); });
const [statusOpen, statusHandlers] = useDisclosure(false, {
onOpen: () => {
setStatus(record?.status || undefined);
props.changeFn(props.idx, 'status', record?.status || undefined);
},
onClose: () => {
setStatus(undefined);
props.changeFn(props.idx, 'status', undefined);
}
});
const stockString: string = useMemo(() => { const stockString: string = useMemo(() => {
if (!record) { if (!record) {
return '-'; return '-';
@ -481,6 +509,7 @@ function StockOperationsRow({
<> <>
<Table.Tr> <Table.Tr>
<Table.Td> <Table.Td>
<Stack gap='xs'>
<Flex gap='sm' align='center'> <Flex gap='sm' align='center'>
<Thumbnail <Thumbnail
size={40} size={40}
@ -489,6 +518,12 @@ function StockOperationsRow({
/> />
<div>{record.part_detail?.name}</div> <div>{record.part_detail?.name}</div>
</Flex> </Flex>
{props.rowErrors?.pk?.message && (
<Text c='red' size='xs'>
{props.rowErrors.pk.message}
</Text>
)}
</Stack>
</Table.Td> </Table.Td>
<Table.Td> <Table.Td>
{record.location ? record.location_detail?.pathstring : '-'} {record.location ? record.location_detail?.pathstring : '-'}
@ -531,6 +566,15 @@ function StockOperationsRow({
} }
/> />
)} )}
{changeStatus && (
<ActionButton
size='sm'
icon={<InvenTreeIcon icon='status' />}
tooltip={t`Change Status`}
onClick={() => statusHandlers.toggle()}
variant={statusOpen ? 'filled' : 'transparent'}
/>
)}
{transfer && ( {transfer && (
<ActionButton <ActionButton
size='sm' size='sm'
@ -544,12 +588,30 @@ function StockOperationsRow({
</Flex> </Flex>
</Table.Td> </Table.Td>
</Table.Tr> </Table.Tr>
{changeStatus && (
<TableFieldExtraRow
visible={statusOpen}
onValueChange={(value: any) => {
setStatus(value);
props.changeFn(props.idx, 'status', value || undefined);
}}
fieldName='status'
fieldDefinition={{
field_type: 'choice',
label: t`Status`,
choices: statusOptions,
value: status
}}
defaultValue={status}
/>
)}
{transfer && ( {transfer && (
<TableFieldExtraRow <TableFieldExtraRow
visible={transfer && packagingOpen} visible={transfer && packagingOpen}
onValueChange={(value: any) => { onValueChange={(value: any) => {
props.changeFn(props.idx, 'packaging', value || undefined); props.changeFn(props.idx, 'packaging', value || undefined);
}} }}
fieldName='packaging'
fieldDefinition={{ fieldDefinition={{
field_type: 'string', field_type: 'string',
label: t`Packaging` label: t`Packaging`
@ -604,19 +666,19 @@ function stockTransferFields(items: any[]): ApiFormFieldSet {
<StockOperationsRow <StockOperationsRow
props={row} props={row}
transfer transfer
changeStatus
setMax setMax
key={record.pk} key={record.pk}
record={record} record={record}
/> />
); );
}, },
headers: [t`Part`, t`Location`, t`In Stock`, t`Move`, t`Actions`] headers: [t`Part`, t`Location`, t`Stock`, t`Move`, t`Actions`]
}, },
location: { location: {
filters: { filters: {
structural: false structural: false
} }
// TODO: icon
}, },
notes: {} notes: {}
}; };
@ -641,6 +703,7 @@ function stockRemoveFields(items: any[]): ApiFormFieldSet {
<StockOperationsRow <StockOperationsRow
props={row} props={row}
setMax setMax
changeStatus
add add
key={record.pk} key={record.pk}
record={record} record={record}
@ -670,7 +733,13 @@ function stockAddFields(items: any[]): ApiFormFieldSet {
const record = records[row.item.pk]; const record = records[row.item.pk];
return ( return (
<StockOperationsRow props={row} add key={record.pk} record={record} /> <StockOperationsRow
changeStatus
props={row}
add
key={record.pk}
record={record}
/>
); );
}, },
headers: [t`Part`, t`Location`, t`In Stock`, t`Add`, t`Actions`] headers: [t`Part`, t`Location`, t`In Stock`, t`Add`, t`Actions`]
@ -696,6 +765,7 @@ function stockCountFields(items: any[]): ApiFormFieldSet {
return ( return (
<StockOperationsRow <StockOperationsRow
props={row} props={row}
changeStatus
key={row.item.pk} key={row.item.pk}
record={records[row.item.pk]} record={records[row.item.pk]}
/> />
@ -763,6 +833,7 @@ function stockMergeFields(items: any[]): ApiFormFieldSet {
props={row} props={row}
key={row.item.item} key={row.item.item}
merge merge
changeStatus
record={records[row.item.item]} record={records[row.item.item]}
/> />
); );

View File

@ -653,7 +653,15 @@ export default function StockDetail() {
}); });
const stockActions = useMemo(() => { const stockActions = useMemo(() => {
const inStock = stockitem.in_stock; const inStock =
user.hasChangeRole(UserRoles.stock) &&
stockitem.quantity > 0 &&
!stockitem.sales_order &&
!stockitem.belongs_to &&
!stockitem.customer &&
!stockitem.consumed_by &&
!stockitem.is_building;
const serial = stockitem.serial; const serial = stockitem.serial;
const serialized = const serialized =
serial != null && serial != null &&

View File

@ -37,6 +37,18 @@ export const clearTableFilters = async (page) => {
await page.getByLabel('filter-drawer-close').click(); await page.getByLabel('filter-drawer-close').click();
}; };
export const setTableChoiceFilter = async (page, filter, value) => {
await openFilterDrawer(page);
await page.getByRole('button', { name: 'Add Filter' }).click();
await page.getByPlaceholder('Select filter').fill(filter);
await page.getByRole('option', { name: 'Status' }).click();
await page.getByPlaceholder('Select filter value').click();
await page.getByRole('option', { name: value }).click();
await closeFilterDrawer(page);
};
/** /**
* Return the parent 'row' element for a given 'cell' element * Return the parent 'row' element for a given 'cell' element
* @param cell - The cell element * @param cell - The cell element

View File

@ -1,8 +1,9 @@
import { test } from '../baseFixtures.ts'; import { test } from '../baseFixtures.ts';
import { baseUrl } from '../defaults.ts'; import { baseUrl } from '../defaults.ts';
import { clearTableFilters, setTableChoiceFilter } from '../helpers.ts';
import { doQuickLogin } from '../login.ts'; import { doQuickLogin } from '../login.ts';
test('Sales Orders', async ({ page }) => { test('Sales Orders - Basic Tests', async ({ page }) => {
await doQuickLogin(page); await doQuickLogin(page);
await page.goto(`${baseUrl}/home`); await page.goto(`${baseUrl}/home`);
@ -11,7 +12,11 @@ test('Sales Orders', async ({ page }) => {
// Check for expected text in the table // Check for expected text in the table
await page.getByRole('tab', { name: 'Sales Orders' }).waitFor(); await page.getByRole('tab', { name: 'Sales Orders' }).waitFor();
await page.getByText('In Progress').first().waitFor();
await clearTableFilters(page);
await setTableChoiceFilter(page, 'status', 'On Hold');
await page.getByText('On Hold').first().waitFor(); await page.getByText('On Hold').first().waitFor();
// Navigate to a particular sales order // Navigate to a particular sales order

View File

@ -182,10 +182,22 @@ test('Stock - Stock Actions', async ({ page }) => {
await page.getByLabel('action-menu-stock-operations-count').waitFor(); await page.getByLabel('action-menu-stock-operations-count').waitFor();
await page.getByLabel('action-menu-stock-operations-add').waitFor(); await page.getByLabel('action-menu-stock-operations-add').waitFor();
await page.getByLabel('action-menu-stock-operations-remove').waitFor(); await page.getByLabel('action-menu-stock-operations-remove').waitFor();
await page.getByLabel('action-menu-stock-operations-transfer').click(); await page.getByLabel('action-menu-stock-operations-transfer').click();
await page.getByLabel('text-field-notes').fill('test notes'); await page.getByLabel('text-field-notes').fill('test notes');
await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('button', { name: 'Submit' }).click();
await page.getByText('This field is required.').first().waitFor(); await page.getByText('This field is required.').first().waitFor();
// Set the status field
await page.getByLabel('action-button-change-status').click();
await page.getByLabel('choice-field-status').click();
await page.getByText('Attention needed').click();
// Set the packaging field
await page.getByLabel('action-button-adjust-packaging').click();
await page.getByLabel('text-field-packaging').fill('test packaging');
// Close the dialog
await page.getByRole('button', { name: 'Cancel' }).click(); await page.getByRole('button', { name: 'Cancel' }).click();
// Find an item which has been sent to a customer // Find an item which has been sent to a customer