From b8c20e58cc0be5088eed9ca9e1c2e8863e66b84c Mon Sep 17 00:00:00 2001 From: Mahek Patel Date: Sun, 21 Jun 2026 16:11:47 -0400 Subject: [PATCH 1/6] feat: display linked barcode on stock detail page --- src/backend/InvenTree/stock/serializers.py | 2 ++ src/frontend/src/pages/stock/StockDetail.tsx | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/src/backend/InvenTree/stock/serializers.py b/src/backend/InvenTree/stock/serializers.py index 4943dd6dfee6..facdf297b15f 100644 --- a/src/backend/InvenTree/stock/serializers.py +++ b/src/backend/InvenTree/stock/serializers.py @@ -374,6 +374,7 @@ class Meta: 'SKU', 'MPN', 'barcode_hash', + 'barcode_data', 'creation_date', 'stocktake_date', 'updated', @@ -399,6 +400,7 @@ class Meta: read_only_fields = [ 'allocated', 'barcode_hash', + 'barcode_data', 'creation_date', 'stocktake_date', 'stocktake_user', diff --git a/src/frontend/src/pages/stock/StockDetail.tsx b/src/frontend/src/pages/stock/StockDetail.tsx index 451b787afa00..f5d368cd705c 100644 --- a/src/frontend/src/pages/stock/StockDetail.tsx +++ b/src/frontend/src/pages/stock/StockDetail.tsx @@ -430,6 +430,14 @@ export default function StockDetail() { label: t`Packaging`, hidden: !stockitem.packaging }, + { + type: 'text', + name: 'barcode_data', + icon: 'barcode', + label: t`Linked Barcode`, + copy: true, + hidden: !stockitem.barcode_data + }, { type: 'text', name: 'updated', From 08a915ff7aeb2dd5f78a9dee2ba1dbf082fa0941 Mon Sep 17 00:00:00 2001 From: Mahek Patel Date: Fri, 10 Jul 2026 19:07:10 -0400 Subject: [PATCH 2/6] test: cover linked barcode display on stock detail (#11745) Add a backend API test asserting the read-only barcode_data field is exposed by StockItemSerializer, and a Playwright test covering the link -> display -> unlink flow on the stock detail page. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/backend/InvenTree/stock/test_api.py | 28 ++++++++++++++++++ src/frontend/tests/pages/pui_stock.spec.ts | 34 ++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/src/backend/InvenTree/stock/test_api.py b/src/backend/InvenTree/stock/test_api.py index 5fe46e689f5c..6b29f5aa605f 100644 --- a/src/backend/InvenTree/stock/test_api.py +++ b/src/backend/InvenTree/stock/test_api.py @@ -1630,6 +1630,34 @@ def test_creation_date_set_on_serialize(self): for new_item in new_items: self.assertIsNotNone(new_item.creation_date) + def test_barcode_data_in_detail_api(self): + """Test that 'barcode_data' (the linked barcode string) is exposed via the API. + + Regression test for #11745: the linked barcode string must be present in + the StockItem serializer output so it can be displayed in the user interface. + It must be read-only (assigned/cleared only via the barcode link endpoints). + """ + item = StockItem.objects.get(pk=1) + url = reverse('api-stock-detail', kwargs={'pk': item.pk}) + + # With no external barcode assigned, the field is present but empty + response = self.get(url, expected_code=200) + self.assertIn('barcode_data', response.data) + self.assertEqual(response.data['barcode_data'], '') + + # Assign a third-party barcode to the stock item + item.assign_barcode(barcode_data='TEST-123') + item.refresh_from_db() + + # The linked barcode string must now be returned by the detail endpoint + response = self.get(url, expected_code=200) + self.assertEqual(response.data['barcode_data'], 'TEST-123') + + # The field must be read-only: a PATCH must not overwrite it + self.patch(url, data={'barcode_data': 'HACKED-VALUE'}, expected_code=200) + item.refresh_from_db() + self.assertEqual(item.barcode_data, 'TEST-123') + def test_stock_item_create_with_supplier_part(self): """Test creation of a StockItem via the API, including SupplierPart data.""" # POST with non-existent supplier part diff --git a/src/frontend/tests/pages/pui_stock.spec.ts b/src/frontend/tests/pages/pui_stock.spec.ts index 7a08db7bbc44..1977f1d4fa27 100644 --- a/src/frontend/tests/pages/pui_stock.spec.ts +++ b/src/frontend/tests/pages/pui_stock.spec.ts @@ -329,6 +329,40 @@ test('Stock - Serialize', async ({ browser }) => { await page.getByRole('button', { name: 'Cancel' }).click(); }); +/** + * Regression test for #11745: the linked barcode string must be visible on the + * stock item detail page. Drives the link -> display -> unlink flow, and checks + * that the "Linked Barcode" field only appears when a barcode is linked. + */ +test('Stock - Linked Barcode Display', async ({ browser }) => { + const page = await doCachedLogin(browser, { url: 'stock/item/232/details' }); + await page.getByText('Stock Item Details').first().waitFor(); + + // With no barcode linked, the "Linked Barcode" field must not be rendered + await expect(page.getByText('Linked Barcode')).toHaveCount(0); + + // Link a custom barcode via the barcode actions dropdown + await page.getByLabel('action-menu-barcode-actions').click(); + await page.getByLabel('action-menu-barcode-actions-link-barcode').click(); + + // Enter the barcode data via the keyboard input and submit + await page.getByLabel('barcode-input-scanner').click(); + await page.getByLabel('barcode-scan-keyboard-input').fill('TEST-123'); + await page.getByRole('button', { name: 'Link', exact: true }).click(); + + // The linked barcode string must now be visible on the detail page + await page.getByText('Linked Barcode').waitFor(); + await page.getByText('TEST-123').waitFor(); + + // Clean up: unlink the barcode so the test is idempotent + await page.getByLabel('action-menu-barcode-actions').click(); + await page.getByLabel('action-menu-barcode-actions-unlink-barcode').click(); + await page.getByRole('button', { name: 'Unlink Barcode' }).click(); + + // Once unlinked, the field must disappear again + await expect(page.getByText('Linked Barcode')).toHaveCount(0); +}); + /** * Test various 'actions' on the stock detail page */ From cb5216aa4022ccd1fc0e6a780709e9a7e3fa838b Mon Sep 17 00:00:00 2001 From: Mahek Patel Date: Fri, 10 Jul 2026 19:16:40 -0400 Subject: [PATCH 3/6] chore: bump API version to 520 for barcode_data field (#11745) --- src/backend/InvenTree/InvenTree/api_version.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 1521f8c5cf25..b3bf96f18909 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,11 +1,14 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 519 +INVENTREE_API_VERSION = 520 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v520 -> 2026-07-10 : https://github.com/inventree/InvenTree/pull/TODO + - Adds read-only "barcode_data" field to the StockItem API endpoint + v519 -> 2026-07-09 : https://github.com/inventree/InvenTree/pull/TODO - Adds optional "roles" and "permissions" fields to the /user/me/ API endpoint, via the "?roles=true" query parameter From f0f389cf8e45f7f316fd4be20ea59f0ed82546ba Mon Sep 17 00:00:00 2001 From: Mahek Patel Date: Sat, 11 Jul 2026 13:26:50 -0400 Subject: [PATCH 4/6] feat: expose read-only barcode_data on all barcode-supporting serializers (#11745) --- .../InvenTree/InvenTree/api_version.py | 2 +- .../InvenTree/InvenTree/serializers.py | 16 ++++++ .../InvenTree/InvenTree/test_serializers.py | 52 +++++++++++++++++++ src/backend/InvenTree/build/serializers.py | 3 ++ src/backend/InvenTree/company/serializers.py | 5 ++ src/backend/InvenTree/order/serializers.py | 5 ++ src/backend/InvenTree/part/serializers.py | 2 + src/backend/InvenTree/stock/serializers.py | 4 +- 8 files changed, 87 insertions(+), 2 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index b3bf96f18909..17ec3b0512bc 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -7,7 +7,7 @@ INVENTREE_API_TEXT = """ v520 -> 2026-07-10 : https://github.com/inventree/InvenTree/pull/TODO - - Adds read-only "barcode_data" field to the StockItem API endpoint + - Adds read-only "barcode_data" field to the API endpoints for all models supporting custom barcodes (Part, StockItem, StockLocation, Build, SupplierPart, ManufacturerPart, PurchaseOrder, SalesOrder, ReturnOrder, TransferOrder, SalesOrderShipment) v519 -> 2026-07-09 : https://github.com/inventree/InvenTree/pull/TODO - Adds optional "roles" and "permissions" fields to the /user/me/ API endpoint, via the "?roles=true" query parameter diff --git a/src/backend/InvenTree/InvenTree/serializers.py b/src/backend/InvenTree/InvenTree/serializers.py index c96d726c2db0..5f55403fe6e5 100644 --- a/src/backend/InvenTree/InvenTree/serializers.py +++ b/src/backend/InvenTree/InvenTree/serializers.py @@ -834,6 +834,22 @@ def __init__(self, *args, **kwargs): self.fields.pop('notes', None) +class BarcodeSerializerMixin(serializers.Serializer): + """Serializer mixin for models which support custom (third-party) barcode assignment. + + Exposes the read-only 'barcode_data' field (the linked barcode string) so that + it can be displayed in the user interface. Applies to any model inheriting from + InvenTree.models.InvenTreeBarcodeMixin (e.g. Part, StockItem, StockLocation). + + Note: The serializer's Meta.fields must include 'barcode_data' for it to appear. + The field is read-only here; barcodes are assigned via the barcode link endpoints. + """ + + barcode_data = serializers.CharField( + read_only=True, label=_('Barcode Data'), help_text=_('Third party barcode data') + ) + + class ContentTypeField(serializers.ChoiceField): """Serializer field which represents a ContentType as 'app_label.model_name'. diff --git a/src/backend/InvenTree/InvenTree/test_serializers.py b/src/backend/InvenTree/InvenTree/test_serializers.py index aeccf73d4b6d..963cfbe29828 100644 --- a/src/backend/InvenTree/InvenTree/test_serializers.py +++ b/src/backend/InvenTree/InvenTree/test_serializers.py @@ -111,3 +111,55 @@ def test_basic_setup(self): self.assertContains(response, 'field_c') self.assertContains(response, 'field_d') self.assertNotContains(response, 'field_e') + + +class BarcodeSerializerMixinTest(InvenTreeAPITestCase): + """Tests for the shared BarcodeSerializerMixin. + + Ref #11745: every model which supports custom (third-party) barcodes must + expose the linked barcode string ('barcode_data') via its API serializer, + as a read-only field, so that it can be displayed in the user interface. + """ + + def test_barcode_data_field_exposed(self): + """'barcode_data' must be present and read-only on every barcode serializer.""" + from build.serializers import BuildSerializer + from company.serializers import ( + ManufacturerPartSerializer, + SupplierPartSerializer, + ) + from order.serializers import ( + PurchaseOrderSerializer, + ReturnOrderSerializer, + SalesOrderSerializer, + SalesOrderShipmentSerializer, + TransferOrderSerializer, + ) + from part.serializers import PartSerializer + from stock.serializers import LocationSerializer, StockItemSerializer + + serializers = [ + PartSerializer, + StockItemSerializer, + LocationSerializer, + BuildSerializer, + ManufacturerPartSerializer, + SupplierPartSerializer, + PurchaseOrderSerializer, + SalesOrderSerializer, + ReturnOrderSerializer, + TransferOrderSerializer, + SalesOrderShipmentSerializer, + ] + + for serializer in serializers: + fields = serializer().fields + self.assertIn( + 'barcode_data', + fields, + f"'barcode_data' missing from {serializer.__name__}", + ) + self.assertTrue( + fields['barcode_data'].read_only, + f"'barcode_data' must be read-only in {serializer.__name__}", + ) diff --git a/src/backend/InvenTree/build/serializers.py b/src/backend/InvenTree/build/serializers.py index d542671e00b8..8ffea42bcaef 100644 --- a/src/backend/InvenTree/build/serializers.py +++ b/src/backend/InvenTree/build/serializers.py @@ -33,6 +33,7 @@ from generic.states.fields import InvenTreeCustomStatusSerializerMixin from InvenTree.mixins import DataImportExportSerializerMixin from InvenTree.serializers import ( + BarcodeSerializerMixin, CustomStatusSerializerMixin, DuplicateOptionsSerializer, FilterableSerializerMixin, @@ -58,6 +59,7 @@ class BuildSerializer( + BarcodeSerializerMixin, CustomStatusSerializerMixin, FilterableSerializerMixin, NotesFieldMixin, @@ -78,6 +80,7 @@ class Meta: 'pk', 'title', 'barcode_hash', + 'barcode_data', 'batch', 'creation_date', 'completed', diff --git a/src/backend/InvenTree/company/serializers.py b/src/backend/InvenTree/company/serializers.py index 809f06710fb8..de39cac695d6 100644 --- a/src/backend/InvenTree/company/serializers.py +++ b/src/backend/InvenTree/company/serializers.py @@ -15,6 +15,7 @@ from InvenTree.mixins import DataImportExportSerializerMixin from InvenTree.ready import isGeneratingSchema from InvenTree.serializers import ( + BarcodeSerializerMixin, DuplicateOptionsSerializer, FilterableSerializerMixin, InvenTreeCurrencySerializer, @@ -231,6 +232,7 @@ class Meta: @register_importer() class ManufacturerPartSerializer( + BarcodeSerializerMixin, FilterableSerializerMixin, DataImportExportSerializerMixin, InvenTreeTaggitSerializer, @@ -257,6 +259,7 @@ class Meta: 'MPN', 'link', 'barcode_hash', + 'barcode_data', 'notes', 'tags', 'parameters', @@ -355,6 +358,7 @@ class Meta: @register_importer() class SupplierPartSerializer( + BarcodeSerializerMixin, FilterableSerializerMixin, DataImportExportSerializerMixin, InvenTreeTaggitSerializer, @@ -398,6 +402,7 @@ class Meta: 'note', 'pk', 'barcode_hash', + 'barcode_data', 'packaging', 'pack_quantity', 'pack_quantity_native', diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py index 04dc6e79f48a..708b7af680eb 100644 --- a/src/backend/InvenTree/order/serializers.py +++ b/src/backend/InvenTree/order/serializers.py @@ -31,6 +31,7 @@ from InvenTree.helpers import extract_serial_numbers, hash_barcode, normalize, str2bool from InvenTree.mixins import DataImportExportSerializerMixin from InvenTree.serializers import ( + BarcodeSerializerMixin, CustomStatusSerializerMixin, DuplicateOptionsSerializer, FilterableSerializerMixin, @@ -69,6 +70,7 @@ class TotalPriceMixin(serializers.Serializer): class AbstractOrderSerializer( + BarcodeSerializerMixin, CustomStatusSerializerMixin, DataImportExportSerializerMixin, InvenTreeTaggitSerializer, @@ -227,6 +229,7 @@ def order_fields(extra_fields): 'status_custom_key', 'notes', 'barcode_hash', + 'barcode_data', 'overdue', 'duplicate', # Extra detail fields @@ -1371,6 +1374,7 @@ def annotate_queryset(queryset): @register_importer() class SalesOrderShipmentSerializer( + BarcodeSerializerMixin, DataImportExportSerializerMixin, FilterableSerializerMixin, InvenTreeTaggitSerializer, @@ -1398,6 +1402,7 @@ class Meta: 'tracking_number', 'invoice_number', 'barcode_hash', + 'barcode_data', 'link', 'notes', # Extra detail fields diff --git a/src/backend/InvenTree/part/serializers.py b/src/backend/InvenTree/part/serializers.py index dae1f796f08c..c7827c39712f 100644 --- a/src/backend/InvenTree/part/serializers.py +++ b/src/backend/InvenTree/part/serializers.py @@ -533,6 +533,7 @@ class Meta: @register_importer() class PartSerializer( + InvenTree.serializers.BarcodeSerializerMixin, InvenTree.serializers.FilterableSerializerMixin, DataImportExportSerializerMixin, InvenTree.serializers.NotesFieldMixin, @@ -555,6 +556,7 @@ class Meta: 'active', 'assembly', 'barcode_hash', + 'barcode_data', 'category', 'category_detail', 'category_path', diff --git a/src/backend/InvenTree/stock/serializers.py b/src/backend/InvenTree/stock/serializers.py index 214abd6d5cf4..501ac19a34cc 100644 --- a/src/backend/InvenTree/stock/serializers.py +++ b/src/backend/InvenTree/stock/serializers.py @@ -309,6 +309,7 @@ def validate(self, data): @register_importer() class StockItemSerializer( + InvenTree.serializers.BarcodeSerializerMixin, CustomStatusSerializerMixin, InvenTree.serializers.FilterableSerializerMixin, DataImportExportSerializerMixin, @@ -400,7 +401,6 @@ class Meta: read_only_fields = [ 'allocated', 'barcode_hash', - 'barcode_data', 'creation_date', 'stocktake_date', 'stocktake_user', @@ -1206,6 +1206,7 @@ class Meta: @register_importer() class LocationSerializer( + InvenTree.serializers.BarcodeSerializerMixin, InvenTree.serializers.FilterableSerializerMixin, DataImportExportSerializerMixin, InvenTree.serializers.InvenTreeTaggitSerializer, @@ -1222,6 +1223,7 @@ class Meta: fields = [ 'pk', 'barcode_hash', + 'barcode_data', 'name', 'level', 'description', From 63e6a581f350df9b60aebc7be76f01e8da6ac186 Mon Sep 17 00:00:00 2001 From: Mahek Patel Date: Sat, 11 Jul 2026 13:27:33 -0400 Subject: [PATCH 5/6] feat: show linked barcode on all detail pages via shared helper (#11745) --- src/frontend/src/components/details/Details.tsx | 17 +++++++++++++++++ src/frontend/src/pages/build/BuildDetail.tsx | 6 ++++-- .../pages/company/ManufacturerPartDetail.tsx | 6 ++++-- .../src/pages/company/SupplierPartDetail.tsx | 6 ++++-- src/frontend/src/pages/part/PartDetail.tsx | 6 ++++-- .../pages/purchasing/PurchaseOrderDetail.tsx | 6 ++++-- .../src/pages/sales/ReturnOrderDetail.tsx | 6 ++++-- .../src/pages/sales/SalesOrderDetail.tsx | 6 ++++-- src/frontend/src/pages/stock/LocationDetail.tsx | 6 ++++-- src/frontend/src/pages/stock/StockDetail.tsx | 14 ++++---------- .../src/pages/stock/TransferOrderDetail.tsx | 6 ++++-- 11 files changed, 57 insertions(+), 28 deletions(-) diff --git a/src/frontend/src/components/details/Details.tsx b/src/frontend/src/components/details/Details.tsx index d221ea543d50..79580bb04744 100644 --- a/src/frontend/src/components/details/Details.tsx +++ b/src/frontend/src/components/details/Details.tsx @@ -54,6 +54,23 @@ export type DetailsField = { type BadgeType = 'owner' | 'user' | 'group'; type ValueFormatterReturn = string | number | null | React.ReactNode; +/** + * Construct a reusable "Linked Barcode" detail field for any model which + * supports custom (third-party) barcode assignment (i.e. exposes 'barcode_data'). + * + * The field is copyable and only rendered when a barcode is linked to the instance. + */ +export function barcodeDataField(instance: any): DetailsField { + return { + type: 'text', + name: 'barcode_data', + icon: 'barcode', + label: t`Linked Barcode`, + copy: true, + hidden: !instance?.barcode_data + }; +} + type StringDetailField = | { type: 'string' | 'text'; diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx index 7fbd0d33077c..42bc8476e5e1 100644 --- a/src/frontend/src/pages/build/BuildDetail.tsx +++ b/src/frontend/src/pages/build/BuildDetail.tsx @@ -29,7 +29,8 @@ import PrimaryActionButton from '../../components/buttons/PrimaryActionButton'; import { PrintingActions } from '../../components/buttons/PrintingActions'; import { type DetailsField, - DetailsTable + DetailsTable, + barcodeDataField } from '../../components/details/Details'; import DetailsBadge from '../../components/details/DetailsBadge'; import { DetailsImage } from '../../components/details/DetailsImage'; @@ -319,7 +320,8 @@ export default function BuildDetail() { model_field: 'reference', model: ModelType.build, hidden: !build.parent - } + }, + barcodeDataField(build) ]; const tr: DetailsField[] = [ diff --git a/src/frontend/src/pages/company/ManufacturerPartDetail.tsx b/src/frontend/src/pages/company/ManufacturerPartDetail.tsx index bbaf85b51215..28157c3f6c01 100644 --- a/src/frontend/src/pages/company/ManufacturerPartDetail.tsx +++ b/src/frontend/src/pages/company/ManufacturerPartDetail.tsx @@ -18,7 +18,8 @@ import type { PanelType } from '@lib/types/Panel'; import AdminButton from '../../components/buttons/AdminButton'; import { type DetailsField, - DetailsTable + DetailsTable, + barcodeDataField } from '../../components/details/Details'; import { DetailsImage } from '../../components/details/DetailsImage'; import { ItemDetailsGrid } from '../../components/details/ItemDetails'; @@ -95,7 +96,8 @@ export default function ManufacturerPartDetail() { copy: true, icon: 'info', hidden: !manufacturerPart.description - } + }, + barcodeDataField(manufacturerPart) ]; const tr: DetailsField[] = [ diff --git a/src/frontend/src/pages/company/SupplierPartDetail.tsx b/src/frontend/src/pages/company/SupplierPartDetail.tsx index 9243b42dd0c3..7e4a806b137e 100644 --- a/src/frontend/src/pages/company/SupplierPartDetail.tsx +++ b/src/frontend/src/pages/company/SupplierPartDetail.tsx @@ -20,7 +20,8 @@ import type { PanelType } from '@lib/types/Panel'; import AdminButton from '../../components/buttons/AdminButton'; import { type DetailsField, - DetailsTable + DetailsTable, + barcodeDataField } from '../../components/details/Details'; import DetailsBadge from '../../components/details/DetailsBadge'; import { DetailsImage } from '../../components/details/DetailsImage'; @@ -125,7 +126,8 @@ export default function SupplierPartDetail() { label: t`Note`, copy: true, hidden: !supplierPart.note - } + }, + barcodeDataField(supplierPart) ]; const bl: DetailsField[] = [ diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index b159930beb0e..c5eb89f09cda 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -54,7 +54,8 @@ import { PrintingActions } from '../../components/buttons/PrintingActions'; import StarredToggleButton from '../../components/buttons/StarredToggleButton'; import { type DetailsField, - DetailsTable + DetailsTable, + barcodeDataField } from '../../components/details/Details'; import DetailsBadge from '../../components/details/DetailsBadge'; import { DetailsImage } from '../../components/details/DetailsImage'; @@ -402,7 +403,8 @@ export default function PartDetail() { external: true, copy: true, hidden: !part.link - } + }, + barcodeDataField(part) ]; // Top right - stock availability information diff --git a/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx b/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx index 43f5e8430122..87523e8c5a4d 100644 --- a/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx +++ b/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx @@ -16,7 +16,8 @@ import PrimaryActionButton from '../../components/buttons/PrimaryActionButton'; import { PrintingActions } from '../../components/buttons/PrintingActions'; import { type DetailsField, - DetailsTable + DetailsTable, + barcodeDataField } from '../../components/details/Details'; import { DetailsImage } from '../../components/details/DetailsImage'; import { ItemDetailsGrid } from '../../components/details/ItemDetails'; @@ -180,7 +181,8 @@ export default function PurchaseOrderDetail() { icon: 'status', hidden: !order.status_custom_key || order.status_custom_key == order.status - } + }, + barcodeDataField(order) ]; const tr: DetailsField[] = [ diff --git a/src/frontend/src/pages/sales/ReturnOrderDetail.tsx b/src/frontend/src/pages/sales/ReturnOrderDetail.tsx index b094826d7c7a..fe6caf3c0441 100644 --- a/src/frontend/src/pages/sales/ReturnOrderDetail.tsx +++ b/src/frontend/src/pages/sales/ReturnOrderDetail.tsx @@ -16,7 +16,8 @@ import PrimaryActionButton from '../../components/buttons/PrimaryActionButton'; import { PrintingActions } from '../../components/buttons/PrintingActions'; import { type DetailsField, - DetailsTable + DetailsTable, + barcodeDataField } from '../../components/details/Details'; import { DetailsImage } from '../../components/details/DetailsImage'; import { ItemDetailsGrid } from '../../components/details/ItemDetails'; @@ -146,7 +147,8 @@ export default function ReturnOrderDetail() { icon: 'status', hidden: !order.status_custom_key || order.status_custom_key == order.status - } + }, + barcodeDataField(order) ]; const tr: DetailsField[] = [ diff --git a/src/frontend/src/pages/sales/SalesOrderDetail.tsx b/src/frontend/src/pages/sales/SalesOrderDetail.tsx index 959ea726d1fe..475638dd0c1e 100644 --- a/src/frontend/src/pages/sales/SalesOrderDetail.tsx +++ b/src/frontend/src/pages/sales/SalesOrderDetail.tsx @@ -22,7 +22,8 @@ import PrimaryActionButton from '../../components/buttons/PrimaryActionButton'; import { PrintingActions } from '../../components/buttons/PrintingActions'; import { type DetailsField, - DetailsTable + DetailsTable, + barcodeDataField } from '../../components/details/Details'; import { DetailsImage } from '../../components/details/DetailsImage'; import { ItemDetailsGrid } from '../../components/details/ItemDetails'; @@ -136,7 +137,8 @@ export default function SalesOrderDetail() { icon: 'status', hidden: !order.status_custom_key || order.status_custom_key == order.status - } + }, + barcodeDataField(order) ]; const tr: DetailsField[] = [ diff --git a/src/frontend/src/pages/stock/LocationDetail.tsx b/src/frontend/src/pages/stock/LocationDetail.tsx index f9bdbad8faaf..2b6fbacfed06 100644 --- a/src/frontend/src/pages/stock/LocationDetail.tsx +++ b/src/frontend/src/pages/stock/LocationDetail.tsx @@ -26,7 +26,8 @@ import { PrintingActions } from '../../components/buttons/PrintingActions'; import OrderCalendar from '../../components/calendar/OrderCalendar'; import { type DetailsField, - DetailsTable + DetailsTable, + barcodeDataField } from '../../components/details/Details'; import { ItemDetailsGrid } from '../../components/details/ItemDetails'; import { @@ -145,7 +146,8 @@ export default function Stock() { label: t`Parent Location`, model: ModelType.stocklocation, hidden: !location?.parent - } + }, + barcodeDataField(location) ]; const right: DetailsField[] = [ diff --git a/src/frontend/src/pages/stock/StockDetail.tsx b/src/frontend/src/pages/stock/StockDetail.tsx index aaf22d52175b..97fe5901dbb0 100644 --- a/src/frontend/src/pages/stock/StockDetail.tsx +++ b/src/frontend/src/pages/stock/StockDetail.tsx @@ -44,7 +44,8 @@ import AdminButton from '../../components/buttons/AdminButton'; import { PrintingActions } from '../../components/buttons/PrintingActions'; import { type DetailsField, - DetailsTable + DetailsTable, + barcodeDataField } from '../../components/details/Details'; import DetailsBadge from '../../components/details/DetailsBadge'; import { DetailsImage } from '../../components/details/DetailsImage'; @@ -198,7 +199,8 @@ export default function StockDetail() { external: true, copy: true, hidden: !stockitem.link - } + }, + barcodeDataField(stockitem) ]; // Top right - available stock information @@ -431,14 +433,6 @@ export default function StockDetail() { label: t`Packaging`, hidden: !stockitem.packaging }, - { - type: 'text', - name: 'barcode_data', - icon: 'barcode', - label: t`Linked Barcode`, - copy: true, - hidden: !stockitem.barcode_data - }, { type: 'date', name: 'creation_date', diff --git a/src/frontend/src/pages/stock/TransferOrderDetail.tsx b/src/frontend/src/pages/stock/TransferOrderDetail.tsx index 39dd9d335415..baa6a736562e 100644 --- a/src/frontend/src/pages/stock/TransferOrderDetail.tsx +++ b/src/frontend/src/pages/stock/TransferOrderDetail.tsx @@ -18,7 +18,8 @@ import PrimaryActionButton from '../../components/buttons/PrimaryActionButton'; import { PrintingActions } from '../../components/buttons/PrintingActions'; import { type DetailsField, - DetailsTable + DetailsTable, + barcodeDataField } from '../../components/details/Details'; import { ItemDetailsGrid } from '../../components/details/ItemDetails'; import { @@ -139,7 +140,8 @@ export default function TransferOrderDetail() { icon: 'status', hidden: !order.status_custom_key || order.status_custom_key == order.status - } + }, + barcodeDataField(order) ]; const tr: DetailsField[] = [ From 162b3146368f25c3861e0e3305ed0e88497ea7eb Mon Sep 17 00:00:00 2001 From: Mahek Patel Date: Tue, 14 Jul 2026 13:06:29 -0400 Subject: [PATCH 6/6] feat: validate BarcodeSerializerMixin configuration on init (#11745) --- .../InvenTree/InvenTree/serializers.py | 44 +++++++++++++++++++ .../InvenTree/InvenTree/test_serializers.py | 29 ++++++++++++ 2 files changed, 73 insertions(+) diff --git a/src/backend/InvenTree/InvenTree/serializers.py b/src/backend/InvenTree/InvenTree/serializers.py index 5f55403fe6e5..894cf9000539 100644 --- a/src/backend/InvenTree/InvenTree/serializers.py +++ b/src/backend/InvenTree/InvenTree/serializers.py @@ -7,6 +7,7 @@ from typing import Optional from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ValidationError as DjangoValidationError from django.core.files.storage import default_storage from django.db import models @@ -843,12 +844,55 @@ class BarcodeSerializerMixin(serializers.Serializer): Note: The serializer's Meta.fields must include 'barcode_data' for it to appear. The field is read-only here; barcodes are assigned via the barcode link endpoints. + + Correct configuration is validated on initialization (see __init__), which raises + ImproperlyConfigured if the mixin is applied incorrectly. """ barcode_data = serializers.CharField( read_only=True, label=_('Barcode Data'), help_text=_('Third party barcode data') ) + def __init__(self, *args, **kwargs): + """Validate that the serializer is correctly configured for barcode data. + + For concrete model serializers this checks that: + - the underlying model supports custom barcodes (InvenTreeBarcodeMixin), and + - 'barcode_data' is included in Meta.fields. + + This guards against the mixin being added to an incompatible serializer, or + being added without exposing the field. + """ + super().__init__(*args, **kwargs) + + meta = getattr(self, 'Meta', None) + model = getattr(meta, 'model', None) + + # Abstract / non-model serializers (e.g. AbstractOrderSerializer) are skipped + if model is None: + return + + # Import here to avoid a circular import at module load time + from InvenTree.models import InvenTreeBarcodeMixin + + if not issubclass(model, InvenTreeBarcodeMixin): + raise ImproperlyConfigured( + f'{self.__class__.__name__} uses BarcodeSerializerMixin, but its model ' + f"'{model.__name__}' does not inherit InvenTreeBarcodeMixin." + ) + + meta_fields = getattr(meta, 'fields', None) + + if ( + meta_fields is not None + and meta_fields != '__all__' + and 'barcode_data' not in meta_fields + ): + raise ImproperlyConfigured( + f'{self.__class__.__name__} uses BarcodeSerializerMixin, but ' + f"'barcode_data' is missing from Meta.fields." + ) + class ContentTypeField(serializers.ChoiceField): """Serializer field which represents a ContentType as 'app_label.model_name'. diff --git a/src/backend/InvenTree/InvenTree/test_serializers.py b/src/backend/InvenTree/InvenTree/test_serializers.py index 963cfbe29828..15c4b1afe637 100644 --- a/src/backend/InvenTree/InvenTree/test_serializers.py +++ b/src/backend/InvenTree/InvenTree/test_serializers.py @@ -2,6 +2,7 @@ from django.contrib import admin from django.contrib.auth.models import User +from django.core.exceptions import ImproperlyConfigured from django.urls import path, reverse from rest_framework.serializers import SerializerMethodField @@ -163,3 +164,31 @@ def test_barcode_data_field_exposed(self): fields['barcode_data'].read_only, f"'barcode_data' must be read-only in {serializer.__name__}", ) + + def test_misconfiguration_raises(self): + """The mixin must reject serializers which are configured incorrectly.""" + from part.models import Part + + # Case 1: the model does not support custom barcodes + class NoBarcodeModelSerializer( + InvenTree.serializers.BarcodeSerializerMixin, + InvenTree.serializers.InvenTreeModelSerializer, + ): + class Meta: + model = User + fields = ['id', 'barcode_data'] + + with self.assertRaises(ImproperlyConfigured): + NoBarcodeModelSerializer() + + # Case 2: 'barcode_data' is omitted from Meta.fields + class MissingFieldSerializer( + InvenTree.serializers.BarcodeSerializerMixin, + InvenTree.serializers.InvenTreeModelSerializer, + ): + class Meta: + model = Part + fields = ['pk'] + + with self.assertRaises(ImproperlyConfigured): + MissingFieldSerializer()