Skip to content
5 changes: 4 additions & 1 deletion src/backend/InvenTree/InvenTree/api_version.py
Original file line number Diff line number Diff line change
@@ -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 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

Expand Down
16 changes: 16 additions & 0 deletions src/backend/InvenTree/InvenTree/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment thread
matmair marked this conversation as resolved.
"""

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'.

Expand Down
52 changes: 52 additions & 0 deletions src/backend/InvenTree/InvenTree/test_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__}",
)
3 changes: 3 additions & 0 deletions src/backend/InvenTree/build/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from generic.states.fields import InvenTreeCustomStatusSerializerMixin
from InvenTree.mixins import DataImportExportSerializerMixin
from InvenTree.serializers import (
BarcodeSerializerMixin,
CustomStatusSerializerMixin,
DuplicateOptionsSerializer,
FilterableSerializerMixin,
Expand All @@ -58,6 +59,7 @@


class BuildSerializer(
BarcodeSerializerMixin,
CustomStatusSerializerMixin,
FilterableSerializerMixin,
NotesFieldMixin,
Expand All @@ -78,6 +80,7 @@ class Meta:
'pk',
'title',
'barcode_hash',
'barcode_data',
'batch',
'creation_date',
'completed',
Expand Down
5 changes: 5 additions & 0 deletions src/backend/InvenTree/company/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from InvenTree.mixins import DataImportExportSerializerMixin
from InvenTree.ready import isGeneratingSchema
from InvenTree.serializers import (
BarcodeSerializerMixin,
DuplicateOptionsSerializer,
FilterableSerializerMixin,
InvenTreeCurrencySerializer,
Expand Down Expand Up @@ -231,6 +232,7 @@ class Meta:

@register_importer()
class ManufacturerPartSerializer(
BarcodeSerializerMixin,
FilterableSerializerMixin,
DataImportExportSerializerMixin,
InvenTreeTaggitSerializer,
Expand All @@ -257,6 +259,7 @@ class Meta:
'MPN',
'link',
'barcode_hash',
'barcode_data',
'notes',
'tags',
'parameters',
Expand Down Expand Up @@ -355,6 +358,7 @@ class Meta:

@register_importer()
class SupplierPartSerializer(
BarcodeSerializerMixin,
FilterableSerializerMixin,
DataImportExportSerializerMixin,
InvenTreeTaggitSerializer,
Expand Down Expand Up @@ -398,6 +402,7 @@ class Meta:
'note',
'pk',
'barcode_hash',
'barcode_data',
'packaging',
'pack_quantity',
'pack_quantity_native',
Expand Down
5 changes: 5 additions & 0 deletions src/backend/InvenTree/order/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -69,6 +70,7 @@ class TotalPriceMixin(serializers.Serializer):


class AbstractOrderSerializer(
BarcodeSerializerMixin,
CustomStatusSerializerMixin,
DataImportExportSerializerMixin,
InvenTreeTaggitSerializer,
Expand Down Expand Up @@ -227,6 +229,7 @@ def order_fields(extra_fields):
'status_custom_key',
'notes',
'barcode_hash',
'barcode_data',
'overdue',
'duplicate',
# Extra detail fields
Expand Down Expand Up @@ -1371,6 +1374,7 @@ def annotate_queryset(queryset):

@register_importer()
class SalesOrderShipmentSerializer(
BarcodeSerializerMixin,
DataImportExportSerializerMixin,
FilterableSerializerMixin,
InvenTreeTaggitSerializer,
Expand Down Expand Up @@ -1398,6 +1402,7 @@ class Meta:
'tracking_number',
'invoice_number',
'barcode_hash',
'barcode_data',
'link',
'notes',
# Extra detail fields
Expand Down
2 changes: 2 additions & 0 deletions src/backend/InvenTree/part/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ class Meta:

@register_importer()
class PartSerializer(
InvenTree.serializers.BarcodeSerializerMixin,
InvenTree.serializers.FilterableSerializerMixin,
DataImportExportSerializerMixin,
InvenTree.serializers.NotesFieldMixin,
Expand All @@ -555,6 +556,7 @@ class Meta:
'active',
'assembly',
'barcode_hash',
'barcode_data',
'category',
'category_detail',
'category_path',
Expand Down
4 changes: 4 additions & 0 deletions src/backend/InvenTree/stock/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ def validate(self, data):

@register_importer()
class StockItemSerializer(
InvenTree.serializers.BarcodeSerializerMixin,
CustomStatusSerializerMixin,
InvenTree.serializers.FilterableSerializerMixin,
DataImportExportSerializerMixin,
Expand Down Expand Up @@ -374,6 +375,7 @@ class Meta:
'SKU',
'MPN',
'barcode_hash',
'barcode_data',
'creation_date',
'stocktake_date',
'updated',
Expand Down Expand Up @@ -1204,6 +1206,7 @@ class Meta:

@register_importer()
class LocationSerializer(
InvenTree.serializers.BarcodeSerializerMixin,
InvenTree.serializers.FilterableSerializerMixin,
DataImportExportSerializerMixin,
InvenTree.serializers.InvenTreeTaggitSerializer,
Expand All @@ -1220,6 +1223,7 @@ class Meta:
fields = [
'pk',
'barcode_hash',
'barcode_data',
'name',
'level',
'description',
Expand Down
28 changes: 28 additions & 0 deletions src/backend/InvenTree/stock/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions src/frontend/src/components/details/Details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SchrodingersGat do we need to do a memo trick here?

return {
type: 'text',
name: 'barcode_data',
icon: 'barcode',
label: t`Linked Barcode`,
copy: true,
hidden: !instance?.barcode_data
};
}

type StringDetailField =
| {
type: 'string' | 'text';
Expand Down
6 changes: 4 additions & 2 deletions src/frontend/src/pages/build/BuildDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -319,7 +320,8 @@ export default function BuildDetail() {
model_field: 'reference',
model: ModelType.build,
hidden: !build.parent
}
},
barcodeDataField(build)
];

const tr: DetailsField[] = [
Expand Down
6 changes: 4 additions & 2 deletions src/frontend/src/pages/company/ManufacturerPartDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -95,7 +96,8 @@ export default function ManufacturerPartDetail() {
copy: true,
icon: 'info',
hidden: !manufacturerPart.description
}
},
barcodeDataField(manufacturerPart)
];

const tr: DetailsField[] = [
Expand Down
6 changes: 4 additions & 2 deletions src/frontend/src/pages/company/SupplierPartDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -125,7 +126,8 @@ export default function SupplierPartDetail() {
label: t`Note`,
copy: true,
hidden: !supplierPart.note
}
},
barcodeDataField(supplierPart)
];

const bl: DetailsField[] = [
Expand Down
6 changes: 4 additions & 2 deletions src/frontend/src/pages/part/PartDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -402,7 +403,8 @@ export default function PartDetail() {
external: true,
copy: true,
hidden: !part.link
}
},
barcodeDataField(part)
];

// Top right - stock availability information
Expand Down
Loading