mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 11:36:44 +00:00
Purchase Order Destination (#8403)
* Add "destination" field to PurchaseOrder * Add 'destination' field to API * Add location to PurchaseOrderDetail page * Display "destination" on PurchaseOrderDetail page * Pre-select location based on selected "destination" * Fix order of reception priority * Auto-expand the per-line destination field * Add "Purchase Order" detail to StockItemDetail page * Bug fix in PurchaseOrderForms * Split playwright tests * Docs updates * Bump API version * Unit test fixes * Fix more tests * Backport to CUI * Use PurchaseOrder destination when scanning items
This commit is contained in:
parent
871cd905f1
commit
c4031dba7f
@ -93,6 +93,14 @@ There are two options to mark items as "received":
|
|||||||
!!! note "Permissions"
|
!!! note "Permissions"
|
||||||
Marking line items as received requires the "Purchase order" ADD permission.
|
Marking line items as received requires the "Purchase order" ADD permission.
|
||||||
|
|
||||||
|
### Item Location
|
||||||
|
|
||||||
|
When receiving items from a purchase order, the location of the items must be specified. There are multiple ways to specify the location:
|
||||||
|
|
||||||
|
* **Order Destination**: The *destination* field of the purchase order can be set to a specific location. When receiving items, the location will default to the destination location.
|
||||||
|
|
||||||
|
* **Line Item Location**: Each line item can have a specific location set. When receiving items, the location will default to the line item location. *Note: A destination specified at the line item level will override the destination specified at the order level.*
|
||||||
|
|
||||||
### Received Items
|
### Received Items
|
||||||
|
|
||||||
Each item marked as "received" is automatically converted into a stock item.
|
Each item marked as "received" is automatically converted into a stock item.
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 275
|
INVENTREE_API_VERSION = 276
|
||||||
|
|
||||||
"""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 = """
|
||||||
|
|
||||||
v274 - 2024-10-31 : https://github.com/inventree/InvenTree/pull/8396
|
v276 - 2024-10-31 : https://github.com/inventree/InvenTree/pull/8403
|
||||||
|
- Adds 'destination' field to the PurchaseOrder model and API endpoints
|
||||||
|
|
||||||
|
v275 - 2024-10-31 : https://github.com/inventree/InvenTree/pull/8396
|
||||||
- Adds SKU and MPN fields to the StockItem serializer
|
- Adds SKU and MPN fields to the StockItem serializer
|
||||||
- Additional export options for the StockItem serializer
|
- Additional export options for the StockItem serializer
|
||||||
|
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
# Generated by Django 4.2.16 on 2024-10-31 02:52
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import django.db.models.deletion
|
||||||
|
import mptt.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('stock', '0113_stockitem_status_custom_key_and_more'),
|
||||||
|
('order', '0101_purchaseorder_status_custom_key_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='purchaseorder',
|
||||||
|
name='destination',
|
||||||
|
field=mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchase_orders', to='stock.stocklocation', verbose_name='Destination', help_text='Destination for received items'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='purchaseorderlineitem',
|
||||||
|
name='destination',
|
||||||
|
field=mptt.fields.TreeForeignKey(blank=True, help_text='Destination for received items', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='po_lines', to='stock.stocklocation', verbose_name='Destination'),
|
||||||
|
),
|
||||||
|
]
|
@ -543,6 +543,16 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
|||||||
help_text=_('Date order was completed'),
|
help_text=_('Date order was completed'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
destination = TreeForeignKey(
|
||||||
|
'stock.StockLocation',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='purchase_orders',
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name=_('Destination'),
|
||||||
|
help_text=_('Destination for received items'),
|
||||||
|
)
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def add_line_item(
|
def add_line_item(
|
||||||
self,
|
self,
|
||||||
@ -1544,7 +1554,7 @@ class PurchaseOrderLineItem(OrderLineItem):
|
|||||||
related_name='po_lines',
|
related_name='po_lines',
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
help_text=_('Where does the Purchaser want this item to be stored?'),
|
help_text=_('Destination for received items'),
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_destination(self):
|
def get_destination(self):
|
||||||
|
@ -318,6 +318,7 @@ class PurchaseOrderSerializer(
|
|||||||
'supplier_name',
|
'supplier_name',
|
||||||
'total_price',
|
'total_price',
|
||||||
'order_currency',
|
'order_currency',
|
||||||
|
'destination',
|
||||||
])
|
])
|
||||||
|
|
||||||
read_only_fields = ['issue_date', 'complete_date', 'creation_date']
|
read_only_fields = ['issue_date', 'complete_date', 'creation_date']
|
||||||
@ -860,6 +861,7 @@ class PurchaseOrderReceiveSerializer(serializers.Serializer):
|
|||||||
location = serializers.PrimaryKeyRelatedField(
|
location = serializers.PrimaryKeyRelatedField(
|
||||||
queryset=stock.models.StockLocation.objects.all(),
|
queryset=stock.models.StockLocation.objects.all(),
|
||||||
many=False,
|
many=False,
|
||||||
|
required=False,
|
||||||
allow_null=True,
|
allow_null=True,
|
||||||
label=_('Location'),
|
label=_('Location'),
|
||||||
help_text=_('Select destination location for received items'),
|
help_text=_('Select destination location for received items'),
|
||||||
@ -873,9 +875,10 @@ class PurchaseOrderReceiveSerializer(serializers.Serializer):
|
|||||||
"""
|
"""
|
||||||
super().validate(data)
|
super().validate(data)
|
||||||
|
|
||||||
|
order = self.context['order']
|
||||||
items = data.get('items', [])
|
items = data.get('items', [])
|
||||||
|
|
||||||
location = data.get('location', None)
|
location = data.get('location', order.destination)
|
||||||
|
|
||||||
if len(items) == 0:
|
if len(items) == 0:
|
||||||
raise ValidationError(_('Line items must be provided'))
|
raise ValidationError(_('Line items must be provided'))
|
||||||
@ -919,15 +922,17 @@ class PurchaseOrderReceiveSerializer(serializers.Serializer):
|
|||||||
order = self.context['order']
|
order = self.context['order']
|
||||||
|
|
||||||
items = data['items']
|
items = data['items']
|
||||||
location = data.get('location', None)
|
|
||||||
|
# Location can be provided, or default to the order destination
|
||||||
|
location = data.get('location', order.destination)
|
||||||
|
|
||||||
# Now we can actually receive the items into stock
|
# Now we can actually receive the items into stock
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
for item in items:
|
for item in items:
|
||||||
# Select location (in descending order of priority)
|
# Select location (in descending order of priority)
|
||||||
loc = (
|
loc = (
|
||||||
location
|
item.get('location', None)
|
||||||
or item.get('location', None)
|
or location
|
||||||
or item['line_item'].get_destination()
|
or item['line_item'].get_destination()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -129,7 +129,15 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% if order.destination %}
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-sitemap'></span></td>
|
||||||
|
<td>{% trans "Destination" %}</td>
|
||||||
|
<td>
|
||||||
|
<a href='{% url "stock-location-detail" order.destination.id %}'>{{ order.destination.name }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{% endblock details %}
|
{% endblock details %}
|
||||||
|
@ -169,6 +169,9 @@ $('#new-po-line').click(function() {
|
|||||||
{{ order.id }},
|
{{ order.id }},
|
||||||
items,
|
items,
|
||||||
{
|
{
|
||||||
|
{% if order.destination %}
|
||||||
|
destination: {{ order.destination.pk }},
|
||||||
|
{% endif %}
|
||||||
success: function() {
|
success: function() {
|
||||||
$("#po-line-table").bootstrapTable('refresh');
|
$("#po-line-table").bootstrapTable('refresh');
|
||||||
}
|
}
|
||||||
|
@ -882,7 +882,6 @@ class PurchaseOrderReceiveTest(OrderTest):
|
|||||||
data = self.post(self.url, {}, expected_code=400).data
|
data = self.post(self.url, {}, expected_code=400).data
|
||||||
|
|
||||||
self.assertIn('This field is required', str(data['items']))
|
self.assertIn('This field is required', str(data['items']))
|
||||||
self.assertIn('This field is required', str(data['location']))
|
|
||||||
|
|
||||||
# No new stock items have been created
|
# No new stock items have been created
|
||||||
self.assertEqual(self.n, StockItem.objects.count())
|
self.assertEqual(self.n, StockItem.objects.count())
|
||||||
@ -1060,9 +1059,9 @@ class PurchaseOrderReceiveTest(OrderTest):
|
|||||||
self.assertEqual(stock_1.count(), 1)
|
self.assertEqual(stock_1.count(), 1)
|
||||||
self.assertEqual(stock_2.count(), 1)
|
self.assertEqual(stock_2.count(), 1)
|
||||||
|
|
||||||
# Same location for each received item, as overall 'location' field is provided
|
# Check received locations
|
||||||
self.assertEqual(stock_1.last().location.pk, 1)
|
self.assertEqual(stock_1.last().location.pk, 1)
|
||||||
self.assertEqual(stock_2.last().location.pk, 1)
|
self.assertEqual(stock_2.last().location.pk, 2)
|
||||||
|
|
||||||
# Barcodes should have been assigned to the stock items
|
# Barcodes should have been assigned to the stock items
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
|
@ -500,6 +500,15 @@ class BarcodePOReceive(BarcodeView):
|
|||||||
purchase_order = kwargs.get('purchase_order')
|
purchase_order = kwargs.get('purchase_order')
|
||||||
location = kwargs.get('location')
|
location = kwargs.get('location')
|
||||||
|
|
||||||
|
# Extract location from PurchaseOrder, if available
|
||||||
|
if not location and purchase_order:
|
||||||
|
try:
|
||||||
|
po = order.models.PurchaseOrder.objects.get(pk=purchase_order)
|
||||||
|
if po.destination:
|
||||||
|
location = po.destination.pk
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
plugins = registry.with_mixin('barcode')
|
plugins = registry.with_mixin('barcode')
|
||||||
|
|
||||||
# Look for a barcode plugin which knows how to deal with this barcode
|
# Look for a barcode plugin which knows how to deal with this barcode
|
||||||
|
@ -35,7 +35,7 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
|
|||||||
'packagename': 'invalid_package_name-asdads-asfd-asdf-asdf-asdf',
|
'packagename': 'invalid_package_name-asdads-asfd-asdf-asdf-asdf',
|
||||||
},
|
},
|
||||||
expected_code=400,
|
expected_code=400,
|
||||||
max_query_time=30,
|
max_query_time=60,
|
||||||
)
|
)
|
||||||
|
|
||||||
# valid - Pypi
|
# valid - Pypi
|
||||||
|
@ -111,6 +111,9 @@ function purchaseOrderFields(options={}) {
|
|||||||
target_date: {
|
target_date: {
|
||||||
icon: 'fa-calendar-alt',
|
icon: 'fa-calendar-alt',
|
||||||
},
|
},
|
||||||
|
destination: {
|
||||||
|
icon: 'fa-sitemap'
|
||||||
|
},
|
||||||
link: {
|
link: {
|
||||||
icon: 'fa-link',
|
icon: 'fa-link',
|
||||||
},
|
},
|
||||||
@ -1361,6 +1364,7 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
fields: {
|
fields: {
|
||||||
location: {
|
location: {
|
||||||
|
value: options.destination,
|
||||||
filters: {
|
filters: {
|
||||||
structural: false,
|
structural: false,
|
||||||
},
|
},
|
||||||
|
@ -166,6 +166,11 @@ export function usePurchaseOrderFields({
|
|||||||
target_date: {
|
target_date: {
|
||||||
icon: <IconCalendar />
|
icon: <IconCalendar />
|
||||||
},
|
},
|
||||||
|
destination: {
|
||||||
|
filters: {
|
||||||
|
structural: false
|
||||||
|
}
|
||||||
|
},
|
||||||
link: {},
|
link: {},
|
||||||
contact: {
|
contact: {
|
||||||
icon: <IconUser />,
|
icon: <IconUser />,
|
||||||
@ -232,6 +237,13 @@ function LineItemFormRow({
|
|||||||
onClose: () => props.changeFn(props.idx, 'location', undefined)
|
onClose: () => props.changeFn(props.idx, 'location', undefined)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!!record.destination) {
|
||||||
|
props.changeFn(props.idx, 'location', record.destination);
|
||||||
|
locationHandlers.open();
|
||||||
|
}
|
||||||
|
}, [record.destination]);
|
||||||
|
|
||||||
// Batch code generator
|
// Batch code generator
|
||||||
const batchCodeGenerator = useBatchCodeGenerator((value: any) => {
|
const batchCodeGenerator = useBatchCodeGenerator((value: any) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
@ -239,7 +251,7 @@ function LineItemFormRow({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Serial numbebr generator
|
// Serial number generator
|
||||||
const serialNumberGenerator = useSerialNumberGenerator((value: any) => {
|
const serialNumberGenerator = useSerialNumberGenerator((value: any) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
props.changeFn(props.idx, 'serial_numbers', value);
|
props.changeFn(props.idx, 'serial_numbers', value);
|
||||||
@ -475,7 +487,7 @@ function LineItemFormRow({
|
|||||||
props.changeFn(props.idx, 'location', value);
|
props.changeFn(props.idx, 'location', value);
|
||||||
},
|
},
|
||||||
description: locationDescription,
|
description: locationDescription,
|
||||||
value: location,
|
value: props.item.location,
|
||||||
label: t`Location`,
|
label: t`Location`,
|
||||||
icon: <InvenTreeIcon icon="location" />
|
icon: <InvenTreeIcon icon="location" />
|
||||||
}}
|
}}
|
||||||
@ -599,6 +611,7 @@ type LineFormHandlers = {
|
|||||||
type LineItemsForm = {
|
type LineItemsForm = {
|
||||||
items: any[];
|
items: any[];
|
||||||
orderPk: number;
|
orderPk: number;
|
||||||
|
destinationPk?: number;
|
||||||
formProps?: LineFormHandlers;
|
formProps?: LineFormHandlers;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -674,7 +687,7 @@ export function useReceiveLineItems(props: LineItemsForm) {
|
|||||||
title: t`Receive Line Items`,
|
title: t`Receive Line Items`,
|
||||||
fields: fields,
|
fields: fields,
|
||||||
initialData: {
|
initialData: {
|
||||||
location: null
|
location: props.destinationPk
|
||||||
},
|
},
|
||||||
size: '80%'
|
size: '80%'
|
||||||
});
|
});
|
||||||
|
@ -188,6 +188,14 @@ export default function BuildDetail() {
|
|||||||
label: t`Completed`,
|
label: t`Completed`,
|
||||||
icon: 'calendar',
|
icon: 'calendar',
|
||||||
hidden: !build.completion_date
|
hidden: !build.completion_date
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'project_code_label',
|
||||||
|
label: t`Project Code`,
|
||||||
|
icon: 'reference',
|
||||||
|
copy: true,
|
||||||
|
hidden: !build.project_code
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -153,12 +153,19 @@ export default function PurchaseOrderDetail() {
|
|||||||
total: order.line_items,
|
total: order.line_items,
|
||||||
progress: order.completed_lines
|
progress: order.completed_lines
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
model: ModelType.stocklocation,
|
||||||
|
link: true,
|
||||||
|
name: 'destination',
|
||||||
|
label: t`Destination`,
|
||||||
|
hidden: !order.destination
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
name: 'currency',
|
name: 'currency',
|
||||||
label: t`Order Currency`,
|
label: t`Order Currency`,
|
||||||
value_formatter: () =>
|
value_formatter: () => orderCurrency
|
||||||
order?.order_currency ?? order?.supplier_detail?.currency
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
@ -190,8 +197,15 @@ export default function PurchaseOrderDetail() {
|
|||||||
icon: 'user',
|
icon: 'user',
|
||||||
copy: true,
|
copy: true,
|
||||||
hidden: !order.contact
|
hidden: !order.contact
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'project_code_label',
|
||||||
|
label: t`Project Code`,
|
||||||
|
icon: 'reference',
|
||||||
|
copy: true,
|
||||||
|
hidden: !order.project_code
|
||||||
}
|
}
|
||||||
// TODO: Project code
|
|
||||||
];
|
];
|
||||||
|
|
||||||
let br: DetailsField[] = [
|
let br: DetailsField[] = [
|
||||||
@ -253,7 +267,7 @@ export default function PurchaseOrderDetail() {
|
|||||||
<DetailsTable fields={br} item={order} />
|
<DetailsTable fields={br} item={order} />
|
||||||
</ItemDetailsGrid>
|
</ItemDetailsGrid>
|
||||||
);
|
);
|
||||||
}, [order, instanceQuery]);
|
}, [order, orderCurrency, instanceQuery]);
|
||||||
|
|
||||||
const orderPanels: PanelType[] = useMemo(() => {
|
const orderPanels: PanelType[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
|
@ -167,8 +167,15 @@ export default function ReturnOrderDetail() {
|
|||||||
icon: 'user',
|
icon: 'user',
|
||||||
copy: true,
|
copy: true,
|
||||||
hidden: !order.contact
|
hidden: !order.contact
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'project_code_label',
|
||||||
|
label: t`Project Code`,
|
||||||
|
icon: 'reference',
|
||||||
|
copy: true,
|
||||||
|
hidden: !order.project_code
|
||||||
}
|
}
|
||||||
// TODO: Project code
|
|
||||||
];
|
];
|
||||||
|
|
||||||
let br: DetailsField[] = [
|
let br: DetailsField[] = [
|
||||||
|
@ -179,8 +179,15 @@ export default function SalesOrderDetail() {
|
|||||||
icon: 'user',
|
icon: 'user',
|
||||||
copy: true,
|
copy: true,
|
||||||
hidden: !order.contact
|
hidden: !order.contact
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'project_code_label',
|
||||||
|
label: t`Project Code`,
|
||||||
|
icon: 'reference',
|
||||||
|
copy: true,
|
||||||
|
hidden: !order.project_code
|
||||||
}
|
}
|
||||||
// TODO: Project code
|
|
||||||
];
|
];
|
||||||
|
|
||||||
let br: DetailsField[] = [
|
let br: DetailsField[] = [
|
||||||
|
@ -247,6 +247,15 @@ export default function StockDetail() {
|
|||||||
hidden: !stockitem.build,
|
hidden: !stockitem.build,
|
||||||
model_field: 'reference'
|
model_field: 'reference'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
name: 'purchase_order',
|
||||||
|
label: t`Purchase Order`,
|
||||||
|
model: ModelType.purchaseorder,
|
||||||
|
hidden: !stockitem.purchase_order,
|
||||||
|
icon: 'purchase_orders',
|
||||||
|
model_field: 'reference'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'link',
|
type: 'link',
|
||||||
name: 'sales_order',
|
name: 'sales_order',
|
||||||
|
@ -113,6 +113,7 @@ export function PurchaseOrderLineItemTable({
|
|||||||
const receiveLineItems = useReceiveLineItems({
|
const receiveLineItems = useReceiveLineItems({
|
||||||
items: singleRecord ? [singleRecord] : table.selectedRecords,
|
items: singleRecord ? [singleRecord] : table.selectedRecords,
|
||||||
orderPk: orderId,
|
orderPk: orderId,
|
||||||
|
destinationPk: order.destination,
|
||||||
formProps: {
|
formProps: {
|
||||||
// Timeout is a small hack to prevent function being called before re-render
|
// Timeout is a small hack to prevent function being called before re-render
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
|
85
src/frontend/tests/pages/pui_purchase_order.spec.ts
Normal file
85
src/frontend/tests/pages/pui_purchase_order.spec.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { test } from '../baseFixtures.ts';
|
||||||
|
import { doQuickLogin } from '../login.ts';
|
||||||
|
|
||||||
|
test('Purchase Orders - General', async ({ page }) => {
|
||||||
|
await doQuickLogin(page);
|
||||||
|
|
||||||
|
await page.getByRole('tab', { name: 'Purchasing' }).click();
|
||||||
|
await page.getByRole('cell', { name: 'PO0012' }).click();
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
|
||||||
|
await page.getByRole('tab', { name: 'Line Items' }).click();
|
||||||
|
await page.getByRole('tab', { name: 'Received Stock' }).click();
|
||||||
|
await page.getByRole('tab', { name: 'Attachments' }).click();
|
||||||
|
await page.getByRole('tab', { name: 'Purchasing' }).click();
|
||||||
|
await page.getByRole('tab', { name: 'Suppliers' }).click();
|
||||||
|
await page.getByText('Arrow', { exact: true }).click();
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
|
||||||
|
await page.getByRole('tab', { name: 'Supplied Parts' }).click();
|
||||||
|
await page.getByRole('tab', { name: 'Purchase Orders' }).click();
|
||||||
|
await page.getByRole('tab', { name: 'Stock Items' }).click();
|
||||||
|
await page.getByRole('tab', { name: 'Contacts' }).click();
|
||||||
|
await page.getByRole('tab', { name: 'Addresses' }).click();
|
||||||
|
await page.getByRole('tab', { name: 'Attachments' }).click();
|
||||||
|
await page.getByRole('tab', { name: 'Purchasing' }).click();
|
||||||
|
await page.getByRole('tab', { name: 'Manufacturers' }).click();
|
||||||
|
await page.getByText('AVX Corporation').click();
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
|
||||||
|
await page.getByRole('tab', { name: 'Addresses' }).click();
|
||||||
|
await page.getByRole('cell', { name: 'West Branch' }).click();
|
||||||
|
await page.locator('.mantine-ScrollArea-root').click();
|
||||||
|
await page
|
||||||
|
.getByRole('row', { name: 'West Branch Yes Surf Avenue 9' })
|
||||||
|
.getByRole('button')
|
||||||
|
.click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Edit' }).click();
|
||||||
|
|
||||||
|
await page.getByLabel('text-field-title').waitFor();
|
||||||
|
await page.getByLabel('text-field-line2').waitFor();
|
||||||
|
|
||||||
|
// Read the current value of the cell, to ensure we always *change* it!
|
||||||
|
const value = await page.getByLabel('text-field-line2').inputValue();
|
||||||
|
await page
|
||||||
|
.getByLabel('text-field-line2')
|
||||||
|
.fill(value == 'old' ? 'new' : 'old');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).isEnabled();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
await page.getByRole('tab', { name: 'Details' }).waitFor();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for receiving items against a purchase order
|
||||||
|
*/
|
||||||
|
test('Purchase Orders - Receive Items', async ({ page }) => {
|
||||||
|
await doQuickLogin(page);
|
||||||
|
|
||||||
|
await page.getByRole('tab', { name: 'Purchasing' }).click();
|
||||||
|
await page.getByRole('cell', { name: 'PO0014' }).click();
|
||||||
|
|
||||||
|
await page.getByRole('tab', { name: 'Order Details' }).click();
|
||||||
|
await page.getByText('0 / 3').waitFor();
|
||||||
|
|
||||||
|
// Select all line items to receive
|
||||||
|
await page.getByRole('tab', { name: 'Line Items' }).click();
|
||||||
|
|
||||||
|
await page.getByLabel('Select all records').click();
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
await page.getByLabel('action-button-receive-items').click();
|
||||||
|
|
||||||
|
// Check for display of individual locations
|
||||||
|
await page
|
||||||
|
.getByRole('cell', { name: /Choose Location/ })
|
||||||
|
.getByText('Parts Bins')
|
||||||
|
.waitFor();
|
||||||
|
await page
|
||||||
|
.getByRole('cell', { name: /Choose Location/ })
|
||||||
|
.getByText('Room 101')
|
||||||
|
.waitFor();
|
||||||
|
await page.getByText('Mechanical Lab').waitFor();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||||
|
});
|
@ -178,53 +178,3 @@ test('Purchase Orders - Barcodes', async ({ page }) => {
|
|||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
await page.getByRole('button', { name: 'Issue Order' }).waitFor();
|
await page.getByRole('button', { name: 'Issue Order' }).waitFor();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Purchase Orders - General', async ({ page }) => {
|
|
||||||
await doQuickLogin(page);
|
|
||||||
|
|
||||||
await page.getByRole('tab', { name: 'Purchasing' }).click();
|
|
||||||
await page.getByRole('cell', { name: 'PO0012' }).click();
|
|
||||||
await page.waitForTimeout(200);
|
|
||||||
|
|
||||||
await page.getByRole('tab', { name: 'Line Items' }).click();
|
|
||||||
await page.getByRole('tab', { name: 'Received Stock' }).click();
|
|
||||||
await page.getByRole('tab', { name: 'Attachments' }).click();
|
|
||||||
await page.getByRole('tab', { name: 'Purchasing' }).click();
|
|
||||||
await page.getByRole('tab', { name: 'Suppliers' }).click();
|
|
||||||
await page.getByText('Arrow', { exact: true }).click();
|
|
||||||
await page.waitForTimeout(200);
|
|
||||||
|
|
||||||
await page.getByRole('tab', { name: 'Supplied Parts' }).click();
|
|
||||||
await page.getByRole('tab', { name: 'Purchase Orders' }).click();
|
|
||||||
await page.getByRole('tab', { name: 'Stock Items' }).click();
|
|
||||||
await page.getByRole('tab', { name: 'Contacts' }).click();
|
|
||||||
await page.getByRole('tab', { name: 'Addresses' }).click();
|
|
||||||
await page.getByRole('tab', { name: 'Attachments' }).click();
|
|
||||||
await page.getByRole('tab', { name: 'Purchasing' }).click();
|
|
||||||
await page.getByRole('tab', { name: 'Manufacturers' }).click();
|
|
||||||
await page.getByText('AVX Corporation').click();
|
|
||||||
await page.waitForTimeout(200);
|
|
||||||
|
|
||||||
await page.getByRole('tab', { name: 'Addresses' }).click();
|
|
||||||
await page.getByRole('cell', { name: 'West Branch' }).click();
|
|
||||||
await page.locator('.mantine-ScrollArea-root').click();
|
|
||||||
await page
|
|
||||||
.getByRole('row', { name: 'West Branch Yes Surf Avenue 9' })
|
|
||||||
.getByRole('button')
|
|
||||||
.click();
|
|
||||||
await page.getByRole('menuitem', { name: 'Edit' }).click();
|
|
||||||
|
|
||||||
await page.getByLabel('text-field-title').waitFor();
|
|
||||||
await page.getByLabel('text-field-line2').waitFor();
|
|
||||||
|
|
||||||
// Read the current value of the cell, to ensure we always *change* it!
|
|
||||||
const value = await page.getByLabel('text-field-line2').inputValue();
|
|
||||||
await page
|
|
||||||
.getByLabel('text-field-line2')
|
|
||||||
.fill(value == 'old' ? 'new' : 'old');
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Submit' }).isEnabled();
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Submit' }).click();
|
|
||||||
await page.getByRole('tab', { name: 'Details' }).waitFor();
|
|
||||||
});
|
|
Loading…
x
Reference in New Issue
Block a user