mirror of
https://github.com/inventree/InvenTree.git
synced 2026-11-14 20:42:42 +00:00
[API] Mark fields as read-only (#13271)
* [API] Mark fields as read-only * update tests * additional ready-only fields
This commit is contained in:
1 parent
790112013b
commit
435cdaafbc
10 files changed
+183
-11
No files matched your search
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- [#13271](https://github.com/inventree/InvenTree/pull/13271) makes a number of API fields read-only, as they are managed by dedicated endpoints and must not be edited directly: `consumed` (BuildLine), `received_date` (ReturnOrderLineItem), `barcode_hash` (ManufacturerPart, SalesOrderShipment), `issue_date` (SalesOrder, ReturnOrder, TransferOrder) and `complete_date` (ReturnOrder, TransferOrder). Values provided for these fields are now ignored.
|
||||
|
||||
### Added
|
||||
|
||||
- [#13266](https://github.com/inventree/InvenTree/pull/13266) Adds a "Manufacturer Barcode" plugin, which provides fallback barcode matching against manufacturer part numbers, and a `BARCODE_PRIORITY` attribute which controls the order in which barcode plugins are queried.
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 578
|
||||
INVENTREE_API_VERSION = 579
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v579 -> 2026-11-14 : https://github.com/inventree/InvenTree/pull/13271
|
||||
- The "consumed" field on the BuildLine API is now read-only - stock must be consumed via the build order endpoints
|
||||
- The "received_date" field on the ReturnOrderLineItem API is now read-only - line items must be received via the return order "receive" endpoint
|
||||
- The "barcode_hash" field on the ManufacturerPart and SalesOrderShipment APIs is now read-only - barcodes must be assigned via the barcode endpoints
|
||||
- The "issue_date" field on the SalesOrder, ReturnOrder and TransferOrder APIs is now read-only (as per the PurchaseOrder API) - it is set when the order is issued
|
||||
- The "complete_date" field on the ReturnOrder and TransferOrder APIs is now read-only (as per the PurchaseOrder API) - it is set when the order is completed
|
||||
|
||||
v578 -> 2026-11-13 : https://github.com/inventree/InvenTree/pull/13265
|
||||
- The "is_building" and "build" fields on the StockItem API are now read-only - build outputs must be created, completed or scrapped via the build order endpoints
|
||||
|
||||
|
||||
@@ -1440,7 +1440,8 @@ class BuildLineSerializer(
|
||||
)
|
||||
|
||||
quantity = serializers.FloatField(label=_('Quantity'))
|
||||
consumed = serializers.FloatField(label=_('Consumed'))
|
||||
# Note: The 'consumed' quantity is only adjusted by consuming allocated stock
|
||||
consumed = serializers.FloatField(label=_('Consumed'), read_only=True)
|
||||
|
||||
bom_item = serializers.PrimaryKeyRelatedField(label=_('BOM Item'), read_only=True)
|
||||
|
||||
|
||||
@@ -1874,6 +1874,28 @@ class BuildOutputCancelTest(BuildAPITest):
|
||||
class BuildLineTests(BuildAPITest):
|
||||
"""Unit tests for the BuildLine API endpoints."""
|
||||
|
||||
def test_consumed_read_only(self):
|
||||
"""The 'consumed' quantity cannot be changed via the BuildLine API.
|
||||
|
||||
Stock must be consumed via the build order endpoints, otherwise
|
||||
the consumed quantity does not match the stock which was actually consumed.
|
||||
"""
|
||||
line = BuildLine.objects.first()
|
||||
self.assertEqual(line.consumed, 0)
|
||||
|
||||
url = reverse('api-build-line-detail', kwargs={'pk': line.pk})
|
||||
|
||||
# The 'consumed' value is ignored, other fields are updated
|
||||
data = self.patch(
|
||||
url, {'consumed': 5, 'quantity': line.quantity + 1}, expected_code=200
|
||||
).data
|
||||
|
||||
self.assertEqual(data['consumed'], 0)
|
||||
self.assertEqual(data['quantity'], line.quantity + 1)
|
||||
|
||||
line.refresh_from_db()
|
||||
self.assertEqual(line.consumed, 0)
|
||||
|
||||
def test_filter_available(self):
|
||||
"""Filter BuildLine objects by 'available' status."""
|
||||
url = reverse('api-build-line-list')
|
||||
|
||||
@@ -269,6 +269,9 @@ class ManufacturerPartSerializer(
|
||||
'parameters',
|
||||
]
|
||||
|
||||
# Note: Barcodes are assigned via the dedicated barcode endpoints
|
||||
read_only_fields = ['barcode_hash']
|
||||
|
||||
tags = common.filters.enable_tags_filter()
|
||||
|
||||
parameters = common.filters.enable_parameters_filter()
|
||||
|
||||
@@ -549,6 +549,24 @@ class ManufacturerTest(InvenTreeAPITestCase):
|
||||
|
||||
roles = ['part.add', 'part.change', 'purchase_order.view']
|
||||
|
||||
def test_barcode_hash_read_only(self):
|
||||
"""The 'barcode_hash' field cannot be changed via the ManufacturerPart API."""
|
||||
part = ManufacturerPart.objects.first()
|
||||
self.assertEqual(part.barcode_hash, '')
|
||||
|
||||
url = reverse('api-manufacturer-part-detail', kwargs={'pk': part.pk})
|
||||
|
||||
# The 'barcode_hash' value is ignored, other fields are updated
|
||||
data = self.patch(
|
||||
url, {'barcode_hash': 'abc123', 'description': 'Updated'}, expected_code=200
|
||||
).data
|
||||
|
||||
self.assertEqual(data['barcode_hash'], '')
|
||||
self.assertEqual(data['description'], 'Updated')
|
||||
|
||||
part.refresh_from_db()
|
||||
self.assertEqual(part.barcode_hash, '')
|
||||
|
||||
def test_manufacturer_part_list(self):
|
||||
"""Test the ManufacturerPart API list functionality."""
|
||||
url = reverse('api-manufacturer-part-list')
|
||||
|
||||
@@ -159,6 +159,8 @@ class AbstractOrderSerializer(
|
||||
|
||||
barcode_hash = serializers.CharField(read_only=True)
|
||||
|
||||
# Note: The creation date may be specified (e.g. when creating a historical order),
|
||||
# otherwise it defaults to the current date when the order is created
|
||||
creation_date = serializers.DateField(
|
||||
required=False, allow_null=True, label=_('Creation Date')
|
||||
)
|
||||
@@ -407,12 +409,7 @@ class PurchaseOrderSerializer(
|
||||
'delivery_address_detail',
|
||||
'updated_at',
|
||||
])
|
||||
read_only_fields = [
|
||||
'issue_date',
|
||||
'complete_date',
|
||||
'creation_date',
|
||||
'updated_at',
|
||||
]
|
||||
read_only_fields = ['issue_date', 'complete_date', 'updated_at']
|
||||
extra_kwargs = {
|
||||
'supplier': {'required': True},
|
||||
'order_currency': {'required': False},
|
||||
@@ -1168,7 +1165,7 @@ class SalesOrderSerializer(
|
||||
'updated_at',
|
||||
'tags',
|
||||
])
|
||||
read_only_fields = ['status', 'creation_date', 'shipment_date', 'updated_at']
|
||||
read_only_fields = ['status', 'issue_date', 'shipment_date', 'updated_at']
|
||||
extra_kwargs = {'order_currency': {'required': False}}
|
||||
|
||||
def skip_create_fields(self):
|
||||
@@ -1493,6 +1490,9 @@ class SalesOrderShipmentSerializer(
|
||||
'tags',
|
||||
]
|
||||
|
||||
# Note: Barcodes are assigned via the dedicated barcode endpoints
|
||||
read_only_fields = ['barcode_hash']
|
||||
|
||||
@staticmethod
|
||||
def annotate_queryset(queryset):
|
||||
"""Annotate the queryset with extra information."""
|
||||
@@ -2209,7 +2209,7 @@ class ReturnOrderSerializer(
|
||||
'total_price',
|
||||
'updated_at',
|
||||
])
|
||||
read_only_fields = ['creation_date', 'updated_at']
|
||||
read_only_fields = ['issue_date', 'complete_date', 'updated_at']
|
||||
|
||||
def skip_create_fields(self):
|
||||
"""Skip these fields when instantiating a new object."""
|
||||
@@ -2416,6 +2416,9 @@ class ReturnOrderLineItemSerializer(
|
||||
'part_detail',
|
||||
])
|
||||
|
||||
# Note: Line items are marked as received via the 'receive' endpoint
|
||||
read_only_fields = ['received_date']
|
||||
|
||||
order_detail = OptionalField(
|
||||
serializer_class=ReturnOrderSerializer,
|
||||
serializer_kwargs={
|
||||
@@ -2505,7 +2508,7 @@ class TransferOrderSerializer(
|
||||
'consume',
|
||||
'complete_date',
|
||||
])
|
||||
read_only_fields = ['creation_date']
|
||||
read_only_fields = ['issue_date', 'complete_date']
|
||||
extra_kwargs = {}
|
||||
|
||||
def skip_create_fields(self):
|
||||
|
||||
@@ -3693,6 +3693,25 @@ class SalesOrderAllocateTest(OrderTest):
|
||||
self.assertEqual(self.shipment.delivery_date, datetime(2023, 12, 5).date())
|
||||
self.assertTrue(self.shipment.is_delivered())
|
||||
|
||||
def test_shipment_barcode_hash_read_only(self):
|
||||
"""The 'barcode_hash' field cannot be changed via the SalesOrderShipment API."""
|
||||
url = reverse('api-so-shipment-detail', kwargs={'pk': self.shipment.pk})
|
||||
|
||||
self.assertEqual(self.shipment.barcode_hash, '')
|
||||
|
||||
# The 'barcode_hash' value is ignored, other fields are updated
|
||||
data = self.patch(
|
||||
url,
|
||||
{'barcode_hash': 'abc123', 'tracking_number': 'TRK999'},
|
||||
expected_code=200,
|
||||
).data
|
||||
|
||||
self.assertEqual(data['barcode_hash'], '')
|
||||
self.assertEqual(data['tracking_number'], 'TRK999')
|
||||
|
||||
self.shipment.refresh_from_db()
|
||||
self.assertEqual(self.shipment.barcode_hash, '')
|
||||
|
||||
def test_shipment_delivery_date(self):
|
||||
"""Test delivery date functions via API."""
|
||||
url = reverse('api-so-shipment-detail', kwargs={'pk': self.shipment.pk})
|
||||
@@ -4620,9 +4639,100 @@ class ReturnOrderTests(InvenTreeAPITestCase):
|
||||
)
|
||||
|
||||
|
||||
class OrderDateReadOnlyTests(InvenTreeAPITestCase):
|
||||
"""The 'issue_date' and 'complete_date' fields are set by issuing / completing an order."""
|
||||
|
||||
fixtures = [
|
||||
'category',
|
||||
'part',
|
||||
'company',
|
||||
'location',
|
||||
'supplier_part',
|
||||
'stock',
|
||||
'order',
|
||||
'sales_order',
|
||||
'return_order',
|
||||
'transfer_order',
|
||||
]
|
||||
|
||||
superuser = True
|
||||
|
||||
def test_dates_read_only(self):
|
||||
"""The 'issue_date' and 'complete_date' fields cannot be changed via the order API."""
|
||||
orders = [
|
||||
(models.PurchaseOrder, 'api-po-detail', ['issue_date', 'complete_date']),
|
||||
(models.SalesOrder, 'api-so-detail', ['issue_date']),
|
||||
(
|
||||
models.ReturnOrder,
|
||||
'api-return-order-detail',
|
||||
['issue_date', 'complete_date'],
|
||||
),
|
||||
(
|
||||
models.TransferOrder,
|
||||
'api-transfer-order-detail',
|
||||
['issue_date', 'complete_date'],
|
||||
),
|
||||
]
|
||||
|
||||
for model, url_name, fields in orders:
|
||||
with self.subTest(model=model.__name__):
|
||||
# Select a "pending" order, which can be freely edited
|
||||
order = model.objects.filter(status=10).first()
|
||||
self.assertIsNotNone(order)
|
||||
|
||||
# Start from a known state, without going via the API
|
||||
# (some fixture orders do not have a valid reference)
|
||||
model.objects.filter(pk=order.pk).update(
|
||||
reference=model.generate_reference(), **dict.fromkeys(fields)
|
||||
)
|
||||
|
||||
url = reverse(url_name, kwargs={'pk': order.pk})
|
||||
|
||||
# The date values are ignored, other fields are updated
|
||||
data = self.patch(
|
||||
url,
|
||||
{**dict.fromkeys(fields, '2024-01-01'), 'description': 'Updated'},
|
||||
expected_code=200,
|
||||
).data
|
||||
|
||||
self.assertEqual(data['description'], 'Updated')
|
||||
|
||||
order.refresh_from_db()
|
||||
|
||||
for field in fields:
|
||||
self.assertIsNone(data[field])
|
||||
self.assertIsNone(getattr(order, field))
|
||||
|
||||
|
||||
class ReturnOrderLineItemTests(InvenTreeAPITestCase):
|
||||
"""Unit tests for ReturnOrderLineItem API endpoints."""
|
||||
|
||||
def test_received_date_read_only(self):
|
||||
"""The 'received_date' field cannot be changed via the ReturnOrderLineItem API.
|
||||
|
||||
Line items must be received via the return order 'receive' endpoint,
|
||||
otherwise the returned stock item is never actually received into stock.
|
||||
"""
|
||||
self.assignRole('return_order.change')
|
||||
|
||||
line = models.ReturnOrderLineItem.objects.filter(received_date=None).first()
|
||||
self.assertIsNotNone(line)
|
||||
|
||||
url = reverse('api-return-order-line-detail', kwargs={'pk': line.pk})
|
||||
|
||||
# The 'received_date' value is ignored, other fields are updated
|
||||
data = self.patch(
|
||||
url,
|
||||
{'received_date': '2024-01-01', 'reference': 'Updated'},
|
||||
expected_code=200,
|
||||
).data
|
||||
|
||||
self.assertIsNone(data['received_date'])
|
||||
self.assertEqual(data['reference'], 'Updated')
|
||||
|
||||
line.refresh_from_db()
|
||||
self.assertIsNone(line.received_date)
|
||||
|
||||
fixtures = [
|
||||
'category',
|
||||
'company',
|
||||
|
||||
@@ -196,6 +196,8 @@ class CategoryTreeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
'subcategories',
|
||||
]
|
||||
|
||||
read_only_fields = ['pathstring']
|
||||
|
||||
subcategories = serializers.IntegerField(label=_('Subcategories'), read_only=True)
|
||||
|
||||
icon = serializers.CharField(
|
||||
@@ -480,6 +482,7 @@ class DefaultLocationSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
|
||||
model = stock_models.StockLocation
|
||||
fields = ['pk', 'name', 'pathstring']
|
||||
read_only_fields = ['pathstring']
|
||||
|
||||
|
||||
@register_importer()
|
||||
|
||||
@@ -190,6 +190,7 @@ class LocationBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
|
||||
model = StockLocation
|
||||
fields = ['pk', 'name', 'pathstring']
|
||||
read_only_fields = ['pathstring']
|
||||
|
||||
|
||||
@register_importer()
|
||||
@@ -1405,6 +1406,8 @@ class LocationTreeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
'sublocations',
|
||||
]
|
||||
|
||||
read_only_fields = ['pathstring']
|
||||
|
||||
sublocations = serializers.IntegerField(label=_('Sublocations'), read_only=True)
|
||||
|
||||
@staticmethod
|
||||
|
||||
Reference in new issue
Block a user