From 698f6fcfdbbfe79ae76fc9c40a418fcd51521826 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 31 Oct 2024 02:53:08 +0000 Subject: [PATCH 01/16] Add "destination" field to PurchaseOrder --- ...0102_purchaseorder_destination_and_more.py | 26 +++++++++++++++++++ src/backend/InvenTree/order/models.py | 11 +++++++- src/backend/InvenTree/plugin/test_api.py | 2 +- 3 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 src/backend/InvenTree/order/migrations/0102_purchaseorder_destination_and_more.py diff --git a/src/backend/InvenTree/order/migrations/0102_purchaseorder_destination_and_more.py b/src/backend/InvenTree/order/migrations/0102_purchaseorder_destination_and_more.py new file mode 100644 index 000000000000..2a7ffac175c2 --- /dev/null +++ b/src/backend/InvenTree/order/migrations/0102_purchaseorder_destination_and_more.py @@ -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 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'), + ), + ] diff --git a/src/backend/InvenTree/order/models.py b/src/backend/InvenTree/order/models.py index 3876822c0734..f74f50741bb0 100644 --- a/src/backend/InvenTree/order/models.py +++ b/src/backend/InvenTree/order/models.py @@ -543,6 +543,15 @@ def company(self): 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 for received items'), + ) + @transaction.atomic def add_line_item( self, @@ -1544,7 +1553,7 @@ def price(self): related_name='po_lines', blank=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): diff --git a/src/backend/InvenTree/plugin/test_api.py b/src/backend/InvenTree/plugin/test_api.py index 3e0c07fc7f4a..2b73bea41675 100644 --- a/src/backend/InvenTree/plugin/test_api.py +++ b/src/backend/InvenTree/plugin/test_api.py @@ -35,7 +35,7 @@ def test_plugin_install(self): 'packagename': 'invalid_package_name-asdads-asfd-asdf-asdf-asdf', }, expected_code=400, - max_query_time=30, + max_query_time=60, ) # valid - Pypi From 3b5a4f5c9737a2df4d4cc5c85088fe523a279f1f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 31 Oct 2024 03:00:56 +0000 Subject: [PATCH 02/16] Add 'destination' field to API --- .../migrations/0102_purchaseorder_destination_and_more.py | 2 +- src/backend/InvenTree/order/models.py | 3 ++- src/backend/InvenTree/order/serializers.py | 1 + .../InvenTree/templates/js/translated/purchase_order.js | 3 +++ src/frontend/src/forms/PurchaseOrderForms.tsx | 1 + 5 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/backend/InvenTree/order/migrations/0102_purchaseorder_destination_and_more.py b/src/backend/InvenTree/order/migrations/0102_purchaseorder_destination_and_more.py index 2a7ffac175c2..9a9ae71d72d1 100644 --- a/src/backend/InvenTree/order/migrations/0102_purchaseorder_destination_and_more.py +++ b/src/backend/InvenTree/order/migrations/0102_purchaseorder_destination_and_more.py @@ -16,7 +16,7 @@ class Migration(migrations.Migration): 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 for received items'), + 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', diff --git a/src/backend/InvenTree/order/models.py b/src/backend/InvenTree/order/models.py index f74f50741bb0..3bcdf3003777 100644 --- a/src/backend/InvenTree/order/models.py +++ b/src/backend/InvenTree/order/models.py @@ -549,7 +549,8 @@ def company(self): related_name='purchase_orders', blank=True, null=True, - verbose_name=_('Destination for received items'), + verbose_name=_('Destination'), + help_text=_('Destination for received items'), ) @transaction.atomic diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py index ed979ac323ec..3c85faec9b2c 100644 --- a/src/backend/InvenTree/order/serializers.py +++ b/src/backend/InvenTree/order/serializers.py @@ -318,6 +318,7 @@ class Meta: 'supplier_name', 'total_price', 'order_currency', + 'destination', ]) read_only_fields = ['issue_date', 'complete_date', 'creation_date'] diff --git a/src/backend/InvenTree/templates/js/translated/purchase_order.js b/src/backend/InvenTree/templates/js/translated/purchase_order.js index 3a320e2d5baf..2491148a90dc 100644 --- a/src/backend/InvenTree/templates/js/translated/purchase_order.js +++ b/src/backend/InvenTree/templates/js/translated/purchase_order.js @@ -111,6 +111,9 @@ function purchaseOrderFields(options={}) { target_date: { icon: 'fa-calendar-alt', }, + destination: { + icon: 'fa-sitemap' + }, link: { icon: 'fa-link', }, diff --git a/src/frontend/src/forms/PurchaseOrderForms.tsx b/src/frontend/src/forms/PurchaseOrderForms.tsx index 4564a169329d..ef051521c19e 100644 --- a/src/frontend/src/forms/PurchaseOrderForms.tsx +++ b/src/frontend/src/forms/PurchaseOrderForms.tsx @@ -166,6 +166,7 @@ export function usePurchaseOrderFields({ target_date: { icon: }, + destination: {}, link: {}, contact: { icon: , From b9f855943ef22f402c95305dfe4bd88627944042 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 31 Oct 2024 03:03:07 +0000 Subject: [PATCH 03/16] Add location to PurchaseOrderDetail page --- src/frontend/src/forms/PurchaseOrderForms.tsx | 6 +++++- src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/frontend/src/forms/PurchaseOrderForms.tsx b/src/frontend/src/forms/PurchaseOrderForms.tsx index ef051521c19e..35fa94a76c49 100644 --- a/src/frontend/src/forms/PurchaseOrderForms.tsx +++ b/src/frontend/src/forms/PurchaseOrderForms.tsx @@ -166,7 +166,11 @@ export function usePurchaseOrderFields({ target_date: { icon: }, - destination: {}, + destination: { + filters: { + structural: false + } + }, link: {}, contact: { icon: , diff --git a/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx b/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx index b7631b984662..6da870b025f7 100644 --- a/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx +++ b/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx @@ -153,6 +153,14 @@ export default function PurchaseOrderDetail() { total: order.line_items, progress: order.completed_lines }, + { + type: 'link', + model: ModelType.stocklocation, + link: true, + name: 'destination', + label: t`Destination`, + hidden: !order.destination + }, { type: 'text', name: 'currency', From ce19ce80d30284fd56db95ddebbe746801eb414e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 31 Oct 2024 05:42:07 +0000 Subject: [PATCH 04/16] Display "destination" on PurchaseOrderDetail page --- src/frontend/src/pages/build/BuildDetail.tsx | 8 ++++++++ .../src/pages/purchasing/PurchaseOrderDetail.tsx | 14 ++++++++++---- src/frontend/src/pages/sales/ReturnOrderDetail.tsx | 9 ++++++++- src/frontend/src/pages/sales/SalesOrderDetail.tsx | 9 ++++++++- 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx index c7c8a62b620f..75d1826bac05 100644 --- a/src/frontend/src/pages/build/BuildDetail.tsx +++ b/src/frontend/src/pages/build/BuildDetail.tsx @@ -188,6 +188,14 @@ export default function BuildDetail() { label: t`Completed`, icon: 'calendar', hidden: !build.completion_date + }, + { + type: 'text', + name: 'project_code_label', + label: t`Project Code`, + icon: 'reference', + copy: true, + hidden: !build.project_code } ]; diff --git a/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx b/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx index 6da870b025f7..36054a07483e 100644 --- a/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx +++ b/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx @@ -165,8 +165,7 @@ export default function PurchaseOrderDetail() { type: 'text', name: 'currency', label: t`Order Currency`, - value_formatter: () => - order?.order_currency ?? order?.supplier_detail?.currency + value_formatter: () => orderCurrency }, { type: 'text', @@ -198,8 +197,15 @@ export default function PurchaseOrderDetail() { icon: 'user', copy: true, 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[] = [ @@ -261,7 +267,7 @@ export default function PurchaseOrderDetail() { ); - }, [order, instanceQuery]); + }, [order, orderCurrency, instanceQuery]); const orderPanels: PanelType[] = useMemo(() => { return [ diff --git a/src/frontend/src/pages/sales/ReturnOrderDetail.tsx b/src/frontend/src/pages/sales/ReturnOrderDetail.tsx index 3fd7bf5765bb..4542fde44e03 100644 --- a/src/frontend/src/pages/sales/ReturnOrderDetail.tsx +++ b/src/frontend/src/pages/sales/ReturnOrderDetail.tsx @@ -167,8 +167,15 @@ export default function ReturnOrderDetail() { icon: 'user', copy: true, 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[] = [ diff --git a/src/frontend/src/pages/sales/SalesOrderDetail.tsx b/src/frontend/src/pages/sales/SalesOrderDetail.tsx index 7e5f94dc0fae..c49c2ba26333 100644 --- a/src/frontend/src/pages/sales/SalesOrderDetail.tsx +++ b/src/frontend/src/pages/sales/SalesOrderDetail.tsx @@ -179,8 +179,15 @@ export default function SalesOrderDetail() { icon: 'user', copy: true, 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[] = [ From a4ecff0f61a3731bf3a947524ad2daa40189745f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 31 Oct 2024 05:47:29 +0000 Subject: [PATCH 05/16] Pre-select location based on selected "destination" --- src/frontend/src/forms/PurchaseOrderForms.tsx | 3 ++- .../src/tables/purchasing/PurchaseOrderLineItemTable.tsx | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/frontend/src/forms/PurchaseOrderForms.tsx b/src/frontend/src/forms/PurchaseOrderForms.tsx index 35fa94a76c49..146a16e2008c 100644 --- a/src/frontend/src/forms/PurchaseOrderForms.tsx +++ b/src/frontend/src/forms/PurchaseOrderForms.tsx @@ -604,6 +604,7 @@ type LineFormHandlers = { type LineItemsForm = { items: any[]; orderPk: number; + destinationPk?: number; formProps?: LineFormHandlers; }; @@ -679,7 +680,7 @@ export function useReceiveLineItems(props: LineItemsForm) { title: t`Receive Line Items`, fields: fields, initialData: { - location: null + location: props.destinationPk }, size: '80%' }); diff --git a/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx b/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx index 60b23a196852..02ffe9797821 100644 --- a/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx +++ b/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx @@ -113,6 +113,7 @@ export function PurchaseOrderLineItemTable({ const receiveLineItems = useReceiveLineItems({ items: singleRecord ? [singleRecord] : table.selectedRecords, orderPk: orderId, + destinationPk: order.destination, formProps: { // Timeout is a small hack to prevent function being called before re-render onClose: () => { From f624c573a4d3e24de005e72f24c25914b087f3fd Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 31 Oct 2024 11:39:36 +0000 Subject: [PATCH 06/16] Fix order of reception priority --- src/backend/InvenTree/order/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py index 3c85faec9b2c..c7c88e5f1bbe 100644 --- a/src/backend/InvenTree/order/serializers.py +++ b/src/backend/InvenTree/order/serializers.py @@ -927,8 +927,8 @@ def save(self): for item in items: # Select location (in descending order of priority) loc = ( - location - or item.get('location', None) + item.get('location', None) + or location or item['line_item'].get_destination() ) From 274c61bc02b911f2e35309d6f90f99ce0731fc6a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 31 Oct 2024 11:39:50 +0000 Subject: [PATCH 07/16] Auto-expand the per-line destination field --- src/frontend/src/forms/PurchaseOrderForms.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/frontend/src/forms/PurchaseOrderForms.tsx b/src/frontend/src/forms/PurchaseOrderForms.tsx index 146a16e2008c..54f074415ee2 100644 --- a/src/frontend/src/forms/PurchaseOrderForms.tsx +++ b/src/frontend/src/forms/PurchaseOrderForms.tsx @@ -237,6 +237,13 @@ function LineItemFormRow({ 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 const batchCodeGenerator = useBatchCodeGenerator((value: any) => { if (value) { From 61a8e58fa1166aacdd816258d729637b14c4f7ca Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 31 Oct 2024 11:40:05 +0000 Subject: [PATCH 08/16] Add "Purchase Order" detail to StockItemDetail page --- src/frontend/src/pages/stock/StockDetail.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/frontend/src/pages/stock/StockDetail.tsx b/src/frontend/src/pages/stock/StockDetail.tsx index 1a9c72368f2f..1a92ba0ec84d 100644 --- a/src/frontend/src/pages/stock/StockDetail.tsx +++ b/src/frontend/src/pages/stock/StockDetail.tsx @@ -247,6 +247,15 @@ export default function StockDetail() { hidden: !stockitem.build, 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', name: 'sales_order', From a1c014cab3ea6b707f9274eee545d32803968790 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 31 Oct 2024 23:08:29 +1100 Subject: [PATCH 09/16] Bug fix in PurchaseOrderForms --- src/frontend/src/forms/PurchaseOrderForms.tsx | 4 +- src/frontend/tests/pages/pui_orders.spec.ts | 230 ------------------ 2 files changed, 2 insertions(+), 232 deletions(-) delete mode 100644 src/frontend/tests/pages/pui_orders.spec.ts diff --git a/src/frontend/src/forms/PurchaseOrderForms.tsx b/src/frontend/src/forms/PurchaseOrderForms.tsx index 54f074415ee2..e0f6b77c9145 100644 --- a/src/frontend/src/forms/PurchaseOrderForms.tsx +++ b/src/frontend/src/forms/PurchaseOrderForms.tsx @@ -251,7 +251,7 @@ function LineItemFormRow({ } }); - // Serial numbebr generator + // Serial number generator const serialNumberGenerator = useSerialNumberGenerator((value: any) => { if (value) { props.changeFn(props.idx, 'serial_numbers', value); @@ -487,7 +487,7 @@ function LineItemFormRow({ props.changeFn(props.idx, 'location', value); }, description: locationDescription, - value: location, + value: props.item.location, label: t`Location`, icon: }} diff --git a/src/frontend/tests/pages/pui_orders.spec.ts b/src/frontend/tests/pages/pui_orders.spec.ts deleted file mode 100644 index 167b9b7751ee..000000000000 --- a/src/frontend/tests/pages/pui_orders.spec.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { test } from '../baseFixtures.ts'; -import { baseUrl } from '../defaults.ts'; -import { doQuickLogin } from '../login.ts'; - -test('Sales Orders', async ({ page }) => { - await doQuickLogin(page); - - await page.goto(`${baseUrl}/home`); - await page.getByRole('tab', { name: 'Sales' }).click(); - await page.getByRole('tab', { name: 'Sales Orders' }).click(); - - // Check for expected text in the table - await page.getByRole('tab', { name: 'Sales Orders' }).waitFor(); - await page.getByText('In Progress').first().waitFor(); - await page.getByText('On Hold').first().waitFor(); - - // Navigate to a particular sales order - await page.getByRole('cell', { name: 'SO0003' }).click(); - - // Order is "on hold". We will "issue" it and then place on hold again - await page.getByText('Sales Order: SO0003').waitFor(); - await page.getByText('On Hold').first().waitFor(); - await page.getByRole('button', { name: 'Issue Order' }).click(); - await page.getByRole('button', { name: 'Submit' }).click(); - - // Order should now be "in progress" - await page.getByText('In Progress').first().waitFor(); - await page.getByRole('button', { name: 'Ship Order' }).waitFor(); - - await page.getByLabel('action-menu-order-actions').click(); - - await page.getByLabel('action-menu-order-actions-edit').waitFor(); - await page.getByLabel('action-menu-order-actions-duplicate').waitFor(); - await page.getByLabel('action-menu-order-actions-cancel').waitFor(); - - // Mark the order as "on hold" again - await page.getByLabel('action-menu-order-actions-hold').click(); - await page.getByRole('button', { name: 'Submit' }).click(); - - await page.getByText('On Hold').first().waitFor(); - await page.getByRole('button', { name: 'Issue Order' }).waitFor(); -}); - -test('Sales Orders - Shipments', async ({ page }) => { - await doQuickLogin(page); - - await page.goto(`${baseUrl}/home`); - await page.getByRole('tab', { name: 'Sales' }).click(); - await page.getByRole('tab', { name: 'Sales Orders' }).click(); - - // Click through to a particular sales order - await page.getByRole('tab', { name: 'Sales Orders' }).waitFor(); - await page.getByRole('cell', { name: 'SO0006' }).first().click(); - await page.getByRole('tab', { name: 'Shipments' }).click(); - - // Create a new shipment - await page.getByLabel('action-button-add-shipment').click(); - await page.getByLabel('text-field-tracking_number').fill('1234567890'); - await page.getByLabel('text-field-invoice_number').fill('9876543210'); - await page.getByRole('button', { name: 'Submit' }).click(); - - // Expected field error - await page - .getByText('The fields order, reference must make a unique set') - .first() - .waitFor(); - await page.getByRole('button', { name: 'Cancel' }).click(); - - // Edit one of the existing shipments - await page.getByLabel('row-action-menu-0').click(); - await page.getByRole('menuitem', { name: 'Edit' }).click(); - - // Ensure the form has loaded - await page.waitForTimeout(500); - - let tracking_number = await page - .getByLabel('text-field-tracking_number') - .inputValue(); - - if (!tracking_number) { - tracking_number = '1234567890'; - } else if (tracking_number.endsWith('x')) { - // Remove the 'x' from the end of the tracking number - tracking_number = tracking_number.substring(0, tracking_number.length - 1); - } else { - // Add an 'x' to the end of the tracking number - tracking_number += 'x'; - } - - // Change the tracking number - await page.getByLabel('text-field-tracking_number').fill(tracking_number); - await page.waitForTimeout(250); - await page.getByRole('button', { name: 'Submit' }).click(); - - // Click through to a particular shipment - await page.getByLabel('row-action-menu-0').click(); - await page.getByRole('menuitem', { name: 'View Shipment' }).click(); - - // Click through the various tabs - await page.getByRole('tab', { name: 'Attachments' }).click(); - await page.getByRole('tab', { name: 'Notes' }).click(); - await page.getByRole('tab', { name: 'Assigned Items' }).click(); - - // Ensure assigned items table loads correctly - await page.getByRole('cell', { name: 'BATCH-001' }).first().waitFor(); - - await page.getByRole('tab', { name: 'Shipment Details' }).click(); - - // The "new" tracking number should be visible - await page.getByText(tracking_number).waitFor(); - - // Link back to sales order - await page.getByRole('link', { name: 'SO0006' }).click(); - - // Let's try to allocate some stock - await page.getByRole('tab', { name: 'Line Items' }).click(); - await page.getByLabel('row-action-menu-1').click(); - await page.getByRole('menuitem', { name: 'Allocate stock' }).click(); - await page - .getByText('Select the source location for the stock allocation') - .waitFor(); - await page.getByLabel('number-field-quantity').fill('123'); - await page.getByLabel('related-field-stock_item').click(); - await page.getByText('Quantity: 42').click(); - await page.getByRole('button', { name: 'Submit' }).click(); - await page.getByText('This field is required.').waitFor(); - await page.getByRole('button', { name: 'Cancel' }).click(); -}); - -test('Purchase Orders', async ({ page }) => { - await doQuickLogin(page); - - await page.goto(`${baseUrl}/home`); - await page.getByRole('tab', { name: 'Purchasing' }).click(); - await page.getByRole('tab', { name: 'Purchase Orders' }).click(); - - // Check for expected values - await page.getByRole('cell', { name: 'PO0014' }).waitFor(); - await page.getByText('Wire-E-Coyote').waitFor(); - await page.getByText('Cancelled').first().waitFor(); - await page.getByText('Pending').first().waitFor(); - await page.getByText('On Hold').first().waitFor(); - - // Click through to a particular purchase order - await page.getByRole('cell', { name: 'PO0013' }).click(); - - await page.getByRole('button', { name: 'Issue Order' }).waitFor(); -}); - -test('Purchase Orders - Barcodes', async ({ page }) => { - await doQuickLogin(page); - - await page.goto(`${baseUrl}/purchasing/purchase-order/13/detail`); - await page.getByRole('button', { name: 'Issue Order' }).waitFor(); - - // Display QR code - await page.getByLabel('action-menu-barcode-actions').click(); - await page.getByLabel('action-menu-barcode-actions-view').click(); - await page.getByRole('img', { name: 'QR Code' }).waitFor(); - await page.getByRole('banner').getByRole('button').click(); - - // Link to barcode - await page.getByLabel('action-menu-barcode-actions').click(); - await page.getByLabel('action-menu-barcode-actions-link-barcode').click(); - await page.getByRole('heading', { name: 'Link Barcode' }).waitFor(); - await page - .getByPlaceholder('Scan barcode data here using') - .fill('1234567890'); - await page.getByRole('button', { name: 'Link' }).click(); - await page.getByRole('button', { name: 'Issue Order' }).waitFor(); - - // Unlink barcode - await page.getByLabel('action-menu-barcode-actions').click(); - await page.getByLabel('action-menu-barcode-actions-unlink-barcode').click(); - await page.getByRole('heading', { name: 'Unlink Barcode' }).waitFor(); - await page.getByText('This will remove the link to').waitFor(); - await page.getByRole('button', { name: 'Unlink Barcode' }).click(); - await page.waitForTimeout(500); - 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(); -}); From 83ef2ebf4f48ef64b372c9de3e936a8bdb41d7ad Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 31 Oct 2024 23:08:38 +1100 Subject: [PATCH 10/16] Split playwright tests --- .../tests/pages/pui_purchase_order.spec.ts | 85 +++++++++ .../tests/pages/pui_sales_order.spec.ts | 180 ++++++++++++++++++ 2 files changed, 265 insertions(+) create mode 100644 src/frontend/tests/pages/pui_purchase_order.spec.ts create mode 100644 src/frontend/tests/pages/pui_sales_order.spec.ts diff --git a/src/frontend/tests/pages/pui_purchase_order.spec.ts b/src/frontend/tests/pages/pui_purchase_order.spec.ts new file mode 100644 index 000000000000..2b94c80ac942 --- /dev/null +++ b/src/frontend/tests/pages/pui_purchase_order.spec.ts @@ -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(); +}); diff --git a/src/frontend/tests/pages/pui_sales_order.spec.ts b/src/frontend/tests/pages/pui_sales_order.spec.ts new file mode 100644 index 000000000000..f4e80b5103b1 --- /dev/null +++ b/src/frontend/tests/pages/pui_sales_order.spec.ts @@ -0,0 +1,180 @@ +import { test } from '../baseFixtures.ts'; +import { baseUrl } from '../defaults.ts'; +import { doQuickLogin } from '../login.ts'; + +test('Sales Orders', async ({ page }) => { + await doQuickLogin(page); + + await page.goto(`${baseUrl}/home`); + await page.getByRole('tab', { name: 'Sales' }).click(); + await page.getByRole('tab', { name: 'Sales Orders' }).click(); + + // Check for expected text in the table + await page.getByRole('tab', { name: 'Sales Orders' }).waitFor(); + await page.getByText('In Progress').first().waitFor(); + await page.getByText('On Hold').first().waitFor(); + + // Navigate to a particular sales order + await page.getByRole('cell', { name: 'SO0003' }).click(); + + // Order is "on hold". We will "issue" it and then place on hold again + await page.getByText('Sales Order: SO0003').waitFor(); + await page.getByText('On Hold').first().waitFor(); + await page.getByRole('button', { name: 'Issue Order' }).click(); + await page.getByRole('button', { name: 'Submit' }).click(); + + // Order should now be "in progress" + await page.getByText('In Progress').first().waitFor(); + await page.getByRole('button', { name: 'Ship Order' }).waitFor(); + + await page.getByLabel('action-menu-order-actions').click(); + + await page.getByLabel('action-menu-order-actions-edit').waitFor(); + await page.getByLabel('action-menu-order-actions-duplicate').waitFor(); + await page.getByLabel('action-menu-order-actions-cancel').waitFor(); + + // Mark the order as "on hold" again + await page.getByLabel('action-menu-order-actions-hold').click(); + await page.getByRole('button', { name: 'Submit' }).click(); + + await page.getByText('On Hold').first().waitFor(); + await page.getByRole('button', { name: 'Issue Order' }).waitFor(); +}); + +test('Sales Orders - Shipments', async ({ page }) => { + await doQuickLogin(page); + + await page.goto(`${baseUrl}/home`); + await page.getByRole('tab', { name: 'Sales' }).click(); + await page.getByRole('tab', { name: 'Sales Orders' }).click(); + + // Click through to a particular sales order + await page.getByRole('tab', { name: 'Sales Orders' }).waitFor(); + await page.getByRole('cell', { name: 'SO0006' }).first().click(); + await page.getByRole('tab', { name: 'Shipments' }).click(); + + // Create a new shipment + await page.getByLabel('action-button-add-shipment').click(); + await page.getByLabel('text-field-tracking_number').fill('1234567890'); + await page.getByLabel('text-field-invoice_number').fill('9876543210'); + await page.getByRole('button', { name: 'Submit' }).click(); + + // Expected field error + await page + .getByText('The fields order, reference must make a unique set') + .first() + .waitFor(); + await page.getByRole('button', { name: 'Cancel' }).click(); + + // Edit one of the existing shipments + await page.getByLabel('row-action-menu-0').click(); + await page.getByRole('menuitem', { name: 'Edit' }).click(); + + // Ensure the form has loaded + await page.waitForTimeout(500); + + let tracking_number = await page + .getByLabel('text-field-tracking_number') + .inputValue(); + + if (!tracking_number) { + tracking_number = '1234567890'; + } else if (tracking_number.endsWith('x')) { + // Remove the 'x' from the end of the tracking number + tracking_number = tracking_number.substring(0, tracking_number.length - 1); + } else { + // Add an 'x' to the end of the tracking number + tracking_number += 'x'; + } + + // Change the tracking number + await page.getByLabel('text-field-tracking_number').fill(tracking_number); + await page.waitForTimeout(250); + await page.getByRole('button', { name: 'Submit' }).click(); + + // Click through to a particular shipment + await page.getByLabel('row-action-menu-0').click(); + await page.getByRole('menuitem', { name: 'View Shipment' }).click(); + + // Click through the various tabs + await page.getByRole('tab', { name: 'Attachments' }).click(); + await page.getByRole('tab', { name: 'Notes' }).click(); + await page.getByRole('tab', { name: 'Assigned Items' }).click(); + + // Ensure assigned items table loads correctly + await page.getByRole('cell', { name: 'BATCH-001' }).first().waitFor(); + + await page.getByRole('tab', { name: 'Shipment Details' }).click(); + + // The "new" tracking number should be visible + await page.getByText(tracking_number).waitFor(); + + // Link back to sales order + await page.getByRole('link', { name: 'SO0006' }).click(); + + // Let's try to allocate some stock + await page.getByRole('tab', { name: 'Line Items' }).click(); + await page.getByLabel('row-action-menu-1').click(); + await page.getByRole('menuitem', { name: 'Allocate stock' }).click(); + await page + .getByText('Select the source location for the stock allocation') + .waitFor(); + await page.getByLabel('number-field-quantity').fill('123'); + await page.getByLabel('related-field-stock_item').click(); + await page.getByText('Quantity: 42').click(); + await page.getByRole('button', { name: 'Submit' }).click(); + await page.getByText('This field is required.').waitFor(); + await page.getByRole('button', { name: 'Cancel' }).click(); +}); + +test('Purchase Orders', async ({ page }) => { + await doQuickLogin(page); + + await page.goto(`${baseUrl}/home`); + await page.getByRole('tab', { name: 'Purchasing' }).click(); + await page.getByRole('tab', { name: 'Purchase Orders' }).click(); + + // Check for expected values + await page.getByRole('cell', { name: 'PO0014' }).waitFor(); + await page.getByText('Wire-E-Coyote').waitFor(); + await page.getByText('Cancelled').first().waitFor(); + await page.getByText('Pending').first().waitFor(); + await page.getByText('On Hold').first().waitFor(); + + // Click through to a particular purchase order + await page.getByRole('cell', { name: 'PO0013' }).click(); + + await page.getByRole('button', { name: 'Issue Order' }).waitFor(); +}); + +test('Purchase Orders - Barcodes', async ({ page }) => { + await doQuickLogin(page); + + await page.goto(`${baseUrl}/purchasing/purchase-order/13/detail`); + await page.getByRole('button', { name: 'Issue Order' }).waitFor(); + + // Display QR code + await page.getByLabel('action-menu-barcode-actions').click(); + await page.getByLabel('action-menu-barcode-actions-view').click(); + await page.getByRole('img', { name: 'QR Code' }).waitFor(); + await page.getByRole('banner').getByRole('button').click(); + + // Link to barcode + await page.getByLabel('action-menu-barcode-actions').click(); + await page.getByLabel('action-menu-barcode-actions-link-barcode').click(); + await page.getByRole('heading', { name: 'Link Barcode' }).waitFor(); + await page + .getByPlaceholder('Scan barcode data here using') + .fill('1234567890'); + await page.getByRole('button', { name: 'Link' }).click(); + await page.getByRole('button', { name: 'Issue Order' }).waitFor(); + + // Unlink barcode + await page.getByLabel('action-menu-barcode-actions').click(); + await page.getByLabel('action-menu-barcode-actions-unlink-barcode').click(); + await page.getByRole('heading', { name: 'Unlink Barcode' }).waitFor(); + await page.getByText('This will remove the link to').waitFor(); + await page.getByRole('button', { name: 'Unlink Barcode' }).click(); + await page.waitForTimeout(500); + await page.getByRole('button', { name: 'Issue Order' }).waitFor(); +}); From 19bedbbf2175e70d536dc0f9873298627ddff4f1 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 31 Oct 2024 23:12:38 +1100 Subject: [PATCH 11/16] Docs updates --- docs/docs/order/purchase_order.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/docs/order/purchase_order.md b/docs/docs/order/purchase_order.md index 7639ddcb039d..349a145936e9 100644 --- a/docs/docs/order/purchase_order.md +++ b/docs/docs/order/purchase_order.md @@ -93,6 +93,14 @@ There are two options to mark items as "received": !!! note "Permissions" 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 Each item marked as "received" is automatically converted into a stock item. From e8c6b74a4bf3b6e89b829270c0ed8b91b84b62b5 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 31 Oct 2024 12:45:13 +0000 Subject: [PATCH 12/16] Bump API version --- src/backend/InvenTree/InvenTree/api_version.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 219c5128dc54..fcd5a4451558 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,14 +1,17 @@ """InvenTree API version information.""" # 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.""" 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 - Additional export options for the StockItem serializer From 56a7f18406b3904db41a1847a4d35fd012cde0b7 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 31 Oct 2024 12:49:32 +0000 Subject: [PATCH 13/16] Unit test fixes --- src/backend/InvenTree/order/serializers.py | 8 ++++++-- src/backend/InvenTree/order/test_api.py | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py index c7c88e5f1bbe..ea65d1c9be9a 100644 --- a/src/backend/InvenTree/order/serializers.py +++ b/src/backend/InvenTree/order/serializers.py @@ -861,6 +861,7 @@ class Meta: location = serializers.PrimaryKeyRelatedField( queryset=stock.models.StockLocation.objects.all(), many=False, + required=False, allow_null=True, label=_('Location'), help_text=_('Select destination location for received items'), @@ -874,9 +875,10 @@ def validate(self, data): """ super().validate(data) + order = self.context['order'] items = data.get('items', []) - location = data.get('location', None) + location = data.get('location', order.destination) if len(items) == 0: raise ValidationError(_('Line items must be provided')) @@ -920,7 +922,9 @@ def save(self): order = self.context['order'] 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 with transaction.atomic(): diff --git a/src/backend/InvenTree/order/test_api.py b/src/backend/InvenTree/order/test_api.py index aba949e7cb44..17f0551404fa 100644 --- a/src/backend/InvenTree/order/test_api.py +++ b/src/backend/InvenTree/order/test_api.py @@ -1060,9 +1060,9 @@ def test_valid(self): self.assertEqual(stock_1.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_2.last().location.pk, 1) + self.assertEqual(stock_2.last().location.pk, 2) # Barcodes should have been assigned to the stock items self.assertTrue( From 01f768c697c6b0a16c12cb0c2582dde6a31968e3 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 31 Oct 2024 13:10:40 +0000 Subject: [PATCH 14/16] Fix more tests --- src/backend/InvenTree/order/test_api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/backend/InvenTree/order/test_api.py b/src/backend/InvenTree/order/test_api.py index 17f0551404fa..e82fd015015a 100644 --- a/src/backend/InvenTree/order/test_api.py +++ b/src/backend/InvenTree/order/test_api.py @@ -882,7 +882,6 @@ def test_empty(self): 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['location'])) # No new stock items have been created self.assertEqual(self.n, StockItem.objects.count()) From cd64b9010cc9dcfc6beb40eaa1d63655ac048e83 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 31 Oct 2024 23:30:52 +0000 Subject: [PATCH 15/16] Backport to CUI --- .../InvenTree/order/templates/order/order_base.html | 10 +++++++++- .../order/templates/order/purchase_order_detail.html | 3 +++ .../templates/js/translated/purchase_order.js | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/backend/InvenTree/order/templates/order/order_base.html b/src/backend/InvenTree/order/templates/order/order_base.html index 2fb284d913ae..9576abb2c121 100644 --- a/src/backend/InvenTree/order/templates/order/order_base.html +++ b/src/backend/InvenTree/order/templates/order/order_base.html @@ -129,7 +129,15 @@ {% endif %} - + {% if order.destination %} + + + {% trans "Destination" %} + + {{ order.destination.name }} + + + {% endif %} {% endblock details %} diff --git a/src/backend/InvenTree/order/templates/order/purchase_order_detail.html b/src/backend/InvenTree/order/templates/order/purchase_order_detail.html index 78da5925ac7d..97e6829d1deb 100644 --- a/src/backend/InvenTree/order/templates/order/purchase_order_detail.html +++ b/src/backend/InvenTree/order/templates/order/purchase_order_detail.html @@ -169,6 +169,9 @@

{% trans "Order Notes" %}

{{ order.id }}, items, { + {% if order.destination %} + destination: {{ order.destination.pk }}, + {% endif %} success: function() { $("#po-line-table").bootstrapTable('refresh'); } diff --git a/src/backend/InvenTree/templates/js/translated/purchase_order.js b/src/backend/InvenTree/templates/js/translated/purchase_order.js index 2491148a90dc..170234b6bbd2 100644 --- a/src/backend/InvenTree/templates/js/translated/purchase_order.js +++ b/src/backend/InvenTree/templates/js/translated/purchase_order.js @@ -1364,6 +1364,7 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) { method: 'POST', fields: { location: { + value: options.destination, filters: { structural: false, }, From da78b83e496e508e293e2c030dcec7ff2794d63c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 31 Oct 2024 23:32:57 +0000 Subject: [PATCH 16/16] Use PurchaseOrder destination when scanning items --- src/backend/InvenTree/plugin/base/barcodes/api.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/backend/InvenTree/plugin/base/barcodes/api.py b/src/backend/InvenTree/plugin/base/barcodes/api.py index 91b64c633701..13d0baaf83f9 100644 --- a/src/backend/InvenTree/plugin/base/barcodes/api.py +++ b/src/backend/InvenTree/plugin/base/barcodes/api.py @@ -500,6 +500,15 @@ def handle_barcode(self, barcode: str, request, **kwargs): purchase_order = kwargs.get('purchase_order') 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') # Look for a barcode plugin which knows how to deal with this barcode