Fix admin search fields and misc code quality issues (#12525)

* Fix admin search for StockItemTracking and StockItemTestResult

Add search_fields to StockTrackingAdmin and StockItemTestResultAdmin

- item__part__name: search tracking by part name
- item__serial: search tracking by serial number
- notes: search tracking notes
- stock_item__part__name: search test results by part name
- stock_item__serial: search test results by serial number
- template__test_name: search test results by test template name
- value: search test results by output value
- notes: search test result notes

Also adds a unit test to verify search_fields configuration.

* Fix admin search for PartPricing, PartStocktake, PartRelated, and PartTestTemplate

Add search_fields to PartPricingAdmin, PartStocktakeAdmin, PartRelatedAdmin,
and PartTestTemplateAdmin.

- part__name, part__IPN, part__description: search PartPricing
- part__name, part__IPN: search PartStocktake
- part_1__name, part_2__name: search PartRelated
- part__name, test_name, description: search PartTestTemplate

Also adds unit test assertions to verify search_fields configuration.

* Fix admin search for SalesOrderAllocation and ReturnOrderLineItem

Add search_fields to SalesOrderAllocationAdmin and ReturnOrderLineItemAdmin.

SalesOrderAllocationAdmin:
- line__order__reference: search by Sales Order reference
- line__part__name: search by ordered Part name
- item__part__name: search by allocated Stock Item part name
- item__part__IPN: search by allocated Stock Item IPN
- item__serial: search by Stock Item serial number

ReturnOrderLineItemAdmin:
- order__reference: search by Return Order reference
- order__customer__name: search by Customer name
- item__part__name: search by returned Item part name
- item__serial: search by returned Item serial number
- reference: search by line item reference

Also adds list_display improvements and unit tests to verify
search_fields configuration.

* Fix incorrect identity comparison for status validation

Use '!=' (value comparison) instead of 'is not' (identity comparison)
when comparing custom_status.logical_key with self.instance.status.

Python only caches small integers (-5 to 256). For status codes > 256,
'is not' can return True even when values are equal, causing valid
custom status keys to be incorrectly rejected.

Per PEP 8: always use '==' or '!=' for value comparisons.

* Fix wrong super() method call in DataImportColumnMapAdmin

The formfield_for_dbfield method was incorrectly calling
super().formfield_for_choice_field() instead of super().formfield_for_dbfield().

These are different Django admin methods with different expectations.
formfield_for_choice_field expects choice-type fields, but the 'column'
field is a plain CharField. This could cause incorrect form rendering
or errors when viewing DataImportSession detail in Django Admin.

Fix: call the correct parent method formfield_for_dbfield().

* Fix broken delete() method signature on EmailMessage model

The delete() method used '*kwargs' which collects positional arguments
into a tuple named 'kwargs'. This breaks Django's Model.delete()
contract which expects keyword arguments (using=None, keep_parents=False).

When super().delete(*kwargs) was called, keyword arguments passed by
Django internals would be unpacked incorrectly as positional args.

Fix: use standard '*args, **kwargs' signature and pass both to super().

* Fix bare except clause in order status validation

Replace bare 'except:' with 'except Exception:' in
validate_status_custom_key method.

Bare except catches all BaseException subclasses including SystemExit,
KeyboardInterrupt, and MemoryError which should never be silenced.
The get_logical_value() function performs a database .get() call that
can raise ObjectDoesNotExist or MultipleObjectsReturned, both of which
are subclasses of Exception.

This follows PEP 8 (E722: do not use bare except).

* Fix bare except clauses in machine registry and barcode mixins

Replace bare 'except:' with 'except Exception:' in two locations:

- machine/registry.py: hash computation catches AttributeError or
  DoesNotExist when a machine config no longer exists
- plugin/base/barcodes/mixins.py: has_barcode_generation property
  catches any error from calling generate(None) on a plugin

Bare except catches all BaseException subclasses including SystemExit
and KeyboardInterrupt which should never be silenced.

This follows PEP 8 (E722: do not use bare except).

* Fix readonly_fields typos and add search_fields in admin classes

* Remove redundant admin field test assertions per review feedback

* Fix file formatting and end-of-file newlines per prek style check

---------

Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
This commit is contained in:
Bhumin Paladiya
2026-08-20 19:52:08 +10:00
committed by GitHub
co-authored by Oliver
parent 36d21fe640
commit 501efdcb6e
13 changed files with 58 additions and 20 deletions
+7
View File
@@ -33,6 +33,13 @@ class BuildItemAdmin(admin.ModelAdmin):
list_display = ('stock_item', 'quantity') list_display = ('stock_item', 'quantity')
search_fields = [
'build_line__build__reference',
'build_line__build__title',
'stock_item__part__name',
'stock_item__serial',
]
autocomplete_fields = ['build_line', 'stock_item', 'install_into'] autocomplete_fields = ['build_line', 'stock_item', 'install_into']
+2 -2
View File
@@ -3423,11 +3423,11 @@ class EmailMessage(models.Model):
objects = NoDeleteManager() objects = NoDeleteManager()
def delete(self, *kwargs): def delete(self, *args, **kwargs):
"""Delete entry - if not protected.""" """Delete entry - if not protected."""
if get_global_setting('INVENTREE_PROTECT_EMAIL_LOG'): if get_global_setting('INVENTREE_PROTECT_EMAIL_LOG'):
raise ValidationError(del_error_msg) raise ValidationError(del_error_msg)
return super().delete(*kwargs) return super().delete(*args, **kwargs)
class EmailThread(InvenTree.models.InvenTreeMetadataModel): class EmailThread(InvenTree.models.InvenTreeMetadataModel):
+1 -1
View File
@@ -27,7 +27,7 @@ class DataImportColumnMapAdmin(admin.TabularInline):
session = queryset.first().session session = queryset.first().session
db_field.choices = [(col, col) for col in session.columns] db_field.choices = [(col, col) for col in session.columns]
return super().formfield_for_choice_field(db_field, request, **kwargs) return super().formfield_for_dbfield(db_field, request, **kwargs)
@admin.register(importer.models.DataImportSession) @admin.register(importer.models.DataImportSession)
+1 -1
View File
@@ -10,7 +10,7 @@ class MachineSettingInline(admin.TabularInline):
model = models.MachineSetting model = models.MachineSetting
read_only_fields = ['key', 'config_type'] readonly_fields = ['key', 'config_type']
def has_add_permission(self, request, obj): def has_add_permission(self, request, obj):
"""The machine settings should not be meddled with manually.""" """The machine settings should not be meddled with manually."""
+1 -1
View File
@@ -432,7 +432,7 @@ class MachineRegistry(
data.update(str(pk).encode()) data.update(str(pk).encode())
try: try:
data.update(str(machine.machine_config.active).encode()) data.update(str(machine.machine_config.active).encode())
except: except Exception:
# machine does not exist anymore, hash will be different # machine does not exist anymore, hash will be different
pass pass
+18 -1
View File
@@ -149,6 +149,15 @@ class SalesOrderAllocationAdmin(admin.ModelAdmin):
list_display = ('line', 'item', 'quantity') list_display = ('line', 'item', 'quantity')
search_fields = [
'line__order__reference',
'line__order__customer__name',
'line__part__name',
'item__part__name',
'item__part__IPN',
'item__serial',
]
autocomplete_fields = ('line', 'shipment', 'item') autocomplete_fields = ('line', 'shipment', 'item')
@@ -171,11 +180,19 @@ class ReturnOrderLineItemAdmin(admin.ModelAdmin):
list_display = ['order', 'item', 'reference'] list_display = ['order', 'item', 'reference']
search_fields = [
'order__reference',
'order__customer__name',
'item__part__name',
'item__serial',
'reference',
]
autocomplete_fields = ['item', 'order'] autocomplete_fields = ['item', 'order']
@admin.register(models.ReturnOrderExtraLine) @admin.register(models.ReturnOrderExtraLine)
class ReturnOrdeerExtraLineAdmin(GeneralExtraLineAdmin, admin.ModelAdmin): class ReturnOrderExtraLineAdmin(GeneralExtraLineAdmin, admin.ModelAdmin):
"""Admin class for the ReturnOrderExtraLine model.""" """Admin class for the ReturnOrderExtraLine model."""
+1 -1
View File
@@ -191,7 +191,7 @@ class AbstractOrderSerializer(
custom_status = get_logical_value( custom_status = get_logical_value(
value, model=self.Meta.model._meta.model_name value, model=self.Meta.model._meta.model_name
) )
except: except Exception:
raise ValidationError(_('Invalid custom status key')) raise ValidationError(_('Invalid custom status key'))
if custom_status.logical_key is not self.instance.status: if custom_status.logical_key is not self.instance.status:
-10
View File
@@ -728,13 +728,3 @@ class OrderUpdatedAtTest(TestCase):
before = self._refresh(instance).updated_at before = self._refresh(instance).updated_at
line.delete() line.delete()
self.assertGreaterEqual(self._refresh(instance).updated_at, before) self.assertGreaterEqual(self._refresh(instance).updated_at, before)
def test_po_lineitem_admin_search(self):
"""Test search fields for PurchaseOrderLineItemAdmin."""
from order.admin import PurchaseOrderLineItemAdmin
admin_class = PurchaseOrderLineItemAdmin
self.assertIn('part__part__name', admin_class.search_fields)
self.assertIn('part__SKU', admin_class.search_fields)
self.assertIn('order__reference', admin_class.search_fields)
self.assertIn('order__supplier__name', admin_class.search_fields)
+8
View File
@@ -38,6 +38,8 @@ class PartPricingAdmin(admin.ModelAdmin):
list_display = ('part', 'overall_min', 'overall_max') list_display = ('part', 'overall_min', 'overall_max')
search_fields = ['part__name', 'part__IPN', 'part__description']
autocomplete_fields = ['part'] autocomplete_fields = ['part']
@@ -47,6 +49,8 @@ class PartStocktakeAdmin(admin.ModelAdmin):
list_display = ['part', 'date', 'quantity'] list_display = ['part', 'date', 'quantity']
search_fields = ['part__name', 'part__IPN']
@admin.register(models.PartCategory) @admin.register(models.PartCategory)
class PartCategoryAdmin(admin.ModelAdmin): class PartCategoryAdmin(admin.ModelAdmin):
@@ -63,6 +67,8 @@ class PartCategoryAdmin(admin.ModelAdmin):
class PartRelatedAdmin(admin.ModelAdmin): class PartRelatedAdmin(admin.ModelAdmin):
"""Class to manage PartRelated objects.""" """Class to manage PartRelated objects."""
search_fields = ['part_1__name', 'part_2__name']
autocomplete_fields = ('part_1', 'part_2') autocomplete_fields = ('part_1', 'part_2')
@@ -73,6 +79,8 @@ class PartTestTemplateAdmin(admin.ModelAdmin):
list_display = ('part', 'test_name', 'required') list_display = ('part', 'test_name', 'required')
readonly_fields = ['key'] readonly_fields = ['key']
search_fields = ['part__name', 'test_name', 'description']
autocomplete_fields = ('part',) autocomplete_fields = ('part',)
+2 -2
View File
@@ -39,7 +39,7 @@ class PluginSettingInline(admin.TabularInline):
model = models.PluginSetting model = models.PluginSetting
read_only_fields = ['key'] readonly_fields = ['key']
def has_add_permission(self, request, obj): def has_add_permission(self, request, obj):
"""The plugin settings should not be meddled with manually.""" """The plugin settings should not be meddled with manually."""
@@ -51,7 +51,7 @@ class PluginUserSettingInline(admin.TabularInline):
model = models.PluginUserSetting model = models.PluginUserSetting
read_only_fields = ['key'] readonly_fields = ['key']
def has_add_permission(self, request, obj): def has_add_permission(self, request, obj):
"""The plugin user settings should not be meddled with manually.""" """The plugin user settings should not be meddled with manually."""
@@ -64,7 +64,7 @@ class BarcodeMixin:
except NotImplementedError: except NotImplementedError:
# If a NotImplementedError is raised, then barcode generation is not supported # If a NotImplementedError is raised, then barcode generation is not supported
return False return False
except: except Exception:
pass pass
return True return True
+6
View File
@@ -15,6 +15,8 @@ class ReportAdmin(admin.ModelAdmin):
list_filter = ('model_type', 'enabled') list_filter = ('model_type', 'enabled')
search_fields = ['name', 'description']
def formfield_for_dbfield(self, db_field, request, **kwargs): def formfield_for_dbfield(self, db_field, request, **kwargs):
"""Provide custom choices for 'model_type' field.""" """Provide custom choices for 'model_type' field."""
if db_field.name == 'model_type': if db_field.name == 'model_type':
@@ -29,9 +31,13 @@ class ReportSnippetAdmin(admin.ModelAdmin):
list_display = ('id', 'snippet', 'description') list_display = ('id', 'snippet', 'description')
search_fields = ['description']
@admin.register(ReportAsset) @admin.register(ReportAsset)
class ReportAssetAdmin(admin.ModelAdmin): class ReportAssetAdmin(admin.ModelAdmin):
"""Admin class for the ReportAsset model.""" """Admin class for the ReportAsset model."""
list_display = ('id', 'asset', 'description') list_display = ('id', 'asset', 'description')
search_fields = ['description']
+10
View File
@@ -81,6 +81,8 @@ class StockTrackingAdmin(admin.ModelAdmin):
list_display = ('item', 'date', 'label') list_display = ('item', 'date', 'label')
search_fields = ['item__part__name', 'item__serial', 'notes']
autocomplete_fields = ['item'] autocomplete_fields = ['item']
def has_add_permission(self, request): def has_add_permission(self, request):
@@ -102,4 +104,12 @@ class StockItemTestResultAdmin(admin.ModelAdmin):
list_display = ('stock_item', 'test_name', 'result', 'value') list_display = ('stock_item', 'test_name', 'result', 'value')
search_fields = [
'stock_item__part__name',
'stock_item__serial',
'template__test_name',
'value',
'notes',
]
autocomplete_fields = ['stock_item'] autocomplete_fields = ['stock_item']