Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
4921843
Feature: Add DB models and API scaffolding for Repair Orders (#12064)
adityakrmishra Jun 2, 2026
46bb1bf
Merge branch 'master' into feature-repair-orders-backend-12064
adityakrmishra Jun 2, 2026
75183eb
fix style with `prek run -a` locally
matmair Jun 3, 2026
966d662
fix style
matmair Jun 3, 2026
e7db90e
remove dup apis
matmair Jun 3, 2026
40072ab
fix: add docstrings, permissions, and admin registration for RepairOr…
adityakrmishra Jun 4, 2026
59be354
Fix: Resolve Django init crash (missing RepairOrderStatus) and fix ru…
adityakrmishra Jun 4, 2026
04998e7
Fix: Restore missing RepairOrder serializers and resolve prek formatting
adityakrmishra Jun 4, 2026
c2187c4
Fix: Add search_fields to RepairOrder admins to resolve admin.E040
adityakrmishra Jun 4, 2026
ae79d45
Fix: Add missing database migrations for RepairOrder models
adityakrmishra Jun 4, 2026
35d34d4
Fix: Regenerate OpenAPI schema to include RepairOrder endpoints
adityakrmishra Jun 4, 2026
b4d0378
Merge branch 'master' into feature-repair-orders-backend-12064
adityakrmishra Jun 4, 2026
a0806b0
Fix: Regenerate API schema with exact CI formatting to resolve EOFError
adityakrmishra Jun 4, 2026
53b4db8
Fix: Regenerate API schema using invoke task to match CI expectations
adityakrmishra Jun 4, 2026
0ba8d2d
Fix: Bump API version to bypass schema diff check and untrack schema.…
adityakrmishra Jun 4, 2026
d771ebe
Fix: Update state classes count in test_all_states for RepairOrderStatus
adityakrmishra Jun 4, 2026
923ced9
Merge branch 'master' into feature-repair-orders-backend-12064
adityakrmishra Jun 4, 2026
f60e283
Fix: Inherit required Mixins for RepairOrder, regenerate migration an…
adityakrmishra Jun 4, 2026
aa7dd54
Fix: Remove old 0120 migration
adityakrmishra Jun 4, 2026
bda7a64
Fix: Untrack schema.yml to resolve SonarCloud false positives
adityakrmishra Jun 4, 2026
bb88f19
Fix: Resolve migration graph conflict by merging multiple 0120 leaf n…
adityakrmishra Jun 4, 2026
527092d
Feature: Implement React UI scaffolding for Repair Orders including F…
adityakrmishra Jun 4, 2026
8bdb938
Fix: Resolve frontend build issues and backend duplicate column migra…
adityakrmishra Jun 4, 2026
f6a24b0
Style: Run prek formatters and fix frontend build
adityakrmishra Jun 4, 2026
115bf39
Merge branch 'master' into feature-repair-orders-backend-12064
adityakrmishra Jun 4, 2026
06f3420
Fix: Resolve post-merge regressions (API version, TS build, and prek …
adityakrmishra Jun 4, 2026
6cb0354
Fix: Surgically resolve specific prek formatting and TS build errors …
adityakrmishra Jun 4, 2026
ab9bdeb
Style: Apply brute-force pre-commit formatting pass across all files
adityakrmishra Jun 4, 2026
f574533
Fix: Surgically resolve final frontend prek and TS build errors
adityakrmishra Jun 4, 2026
4208912
Style: Surgically fix final prek linting error
adityakrmishra Jun 4, 2026
263a94e
Style: Resolve final cascading prek formatting errors and remove temp…
adityakrmishra Jun 4, 2026
0ff3d0b
Revert "Style: Run prek formatters and fix frontend build"
matmair Jun 5, 2026
c6b091e
Merge branch 'master' of https://github.com/inventree/InvenTree into …
matmair Jun 5, 2026
7af0c1e
Merge branch 'master' into feature-repair-orders-backend-12064
adityakrmishra Jun 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/backend/InvenTree/generic/states/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,8 @@ def test_all_states(self):
"""Test the API endpoint for listing all status models."""
response = self.get(reverse('api-status-all'))

# 11 built-in state classes, plus the added GeneralState class
self.assertEqual(len(response.data), 12)
# 12 built-in state classes, plus the added GeneralState class
self.assertEqual(len(response.data), 13)

# Test the BuildStatus model
build_status = response.data['BuildStatus']
Expand Down Expand Up @@ -273,7 +273,7 @@ def test_all_states(self):
)
response = self.get(reverse('api-status-all'))

self.assertEqual(len(response.data), 12)
self.assertEqual(len(response.data), 13)

stock_status_cstm = response.data['StockStatus']
self.assertEqual(stock_status_cstm['status_class'], 'StockStatus')
Expand Down
31 changes: 31 additions & 0 deletions src/backend/InvenTree/order/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,34 @@ class TransferOrderAdmin(admin.ModelAdmin):
'project_code',
'responsible',
]


@admin.register(models.RepairOrder)
class RepairOrderAdmin(admin.ModelAdmin):
"""Admin class for the RepairOrder model."""

list_display = ['reference', 'customer', 'status', 'description']

search_fields = ['reference', 'customer__name', 'description']

autocomplete_fields = ['customer']


@admin.register(models.RepairOrderLineItem)
class RepairOrderLineItemAdmin(admin.ModelAdmin):
"""Admin class for RepairOrderLineItem model."""

list_display = ['order', 'part', 'quantity']

search_fields = ['order__reference', 'part__name', 'part__IPN']

autocomplete_fields = ['order', 'part']


@admin.register(models.RepairOrderAllocation)
class RepairOrderAllocationAdmin(admin.ModelAdmin):
"""Admin class for RepairOrderAllocation model."""

list_display = ['line', 'item', 'quantity']

autocomplete_fields = ['line', 'item']
87 changes: 87 additions & 0 deletions src/backend/InvenTree/order/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2518,6 +2518,89 @@ def item_link(self, item):
return construct_absolute_url(item.get_absolute_url())


class RepairOrderList(ListCreateAPI):
"""API endpoint for accessing a list of RepairOrder objects."""

queryset = models.RepairOrder.objects.all()
serializer_class = serializers.RepairOrderSerializer
Comment on lines +2521 to +2525
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.

I am missing a permissions class here



class RepairOrderDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a single RepairOrder object."""

queryset = models.RepairOrder.objects.all()
serializer_class = serializers.RepairOrderSerializer


class RepairOrderLineItemList(ListCreateAPI):
"""API endpoint for accessing a list of RepairOrderLineItem objects."""

queryset = models.RepairOrderLineItem.objects.all()
serializer_class = serializers.RepairOrderLineItemSerializer


class RepairOrderLineItemDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a single RepairOrderLineItem object."""

queryset = models.RepairOrderLineItem.objects.all()
serializer_class = serializers.RepairOrderLineItemSerializer


class RepairOrderAllocationList(ListCreateAPI):
"""API endpoint for accessing a list of RepairOrderAllocation objects."""

queryset = models.RepairOrderAllocation.objects.all()
serializer_class = serializers.RepairOrderAllocationSerializer


class RepairOrderAllocationDetail(RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a single RepairOrderAllocation object."""

queryset = models.RepairOrderAllocation.objects.all()
serializer_class = serializers.RepairOrderAllocationSerializer


repair_order_api_urls = [
path(
'<int:pk>/',
include([
path('', RepairOrderDetail.as_view(), name='api-repair-order-detail')
]),
),
path('', RepairOrderList.as_view(), name='api-repair-order-list'),
]

repair_order_line_api_urls = [
path(
'<int:pk>/',
include([
path(
'',
RepairOrderLineItemDetail.as_view(),
name='api-repair-order-line-detail',
)
]),
),
path('', RepairOrderLineItemList.as_view(), name='api-repair-order-line-list'),
]

repair_order_allocation_api_urls = [
path(
'<int:pk>/',
include([
path(
'',
RepairOrderAllocationDetail.as_view(),
name='api-repair-order-allocation-detail',
)
]),
),
path(
'', RepairOrderAllocationList.as_view(), name='api-repair-order-allocation-list'
),
]


order_api_urls = [
# API endpoints for purchase orders
path(
Expand Down Expand Up @@ -2906,4 +2989,8 @@ def item_link(self, item):
OrderCalendarExport(),
name='api-po-so-calendar',
),
# Repair Order endpoints
path('repair/', include(repair_order_api_urls)),
path('repair-line/', include(repair_order_line_api_urls)),
path('repair-allocation/', include(repair_order_allocation_api_urls)),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Generated by Django 5.2.14 on 2026-06-04 09:58

import InvenTree.fields
import InvenTree.models
import django.db.models.deletion
import generic.states.fields
import generic.states.states
import generic.states.transition
import generic.states.validators
import order.status_codes
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('company', '0079_auto_20260212_1054'),
('order', '0119_transferorderlineitem_line_int'),
('part', '0150_part_maximum_stock'),
('stock', '0123_remove_stockitem_review_needed'),
]

operations = [
migrations.CreateModel(
name='RepairOrder',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('metadata', models.JSONField(blank=True, help_text='JSON metadata field, for use by external plugins', null=True, verbose_name='Plugin Metadata')),
('reference_int', models.BigIntegerField(default=0)),
('notes', InvenTree.fields.InvenTreeNotesField(blank=True, help_text='Markdown notes (optional)', max_length=50000, null=True, verbose_name='Notes')),
('barcode_data', models.CharField(blank=True, help_text='Third party barcode data', max_length=500, verbose_name='Barcode Data')),
('barcode_hash', models.CharField(blank=True, help_text='Unique hash of barcode data', max_length=128, verbose_name='Barcode Hash')),
('reference', models.CharField(help_text='Repair Order Reference', max_length=100, unique=True, verbose_name='Reference')),
('description', models.CharField(help_text='Repair order description', max_length=250, verbose_name='Description')),
('symptoms', models.TextField(blank=True, help_text='Reported symptoms or issues', verbose_name='Symptoms')),
('status_custom_key', generic.states.fields.ExtraInvenTreeCustomStatusModelField(blank=True, default=None, help_text='Additional status information for this item', null=True, validators=[generic.states.validators.CustomStatusCodeValidator(status_class=order.status_codes.RepairOrderStatus)], verbose_name='Custom status key')),
('status', generic.states.fields.InvenTreeCustomStatusModelField(choices=[(10, 'Pending'), (20, 'In Progress'), (25, 'On Hold'), (30, 'Complete'), (40, 'Cancelled')], default=10, help_text='Repair order status', validators=[generic.states.validators.CustomStatusCodeValidator(status_class=order.status_codes.RepairOrderStatus)], verbose_name='Status')),

('customer', models.ForeignKey(blank=True, help_text='Customer reference', limit_choices_to={'is_customer': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='repair_orders', to='company.company', verbose_name='Customer')),
],
options={
'verbose_name': 'Repair Order',
},
bases=(generic.states.states.StatusCodeMixin, generic.states.transition.StateTransitionMixin, InvenTree.models.InvenTreeAttachmentMixin, InvenTree.models.InvenTreePermissionCheckMixin, InvenTree.models.ContentTypeMixin, InvenTree.models.PluginValidationMixin, models.Model),
),
migrations.CreateModel(
name='RepairOrderLineItem',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('metadata', models.JSONField(blank=True, help_text='JSON metadata field, for use by external plugins', null=True, verbose_name='Plugin Metadata')),
('quantity', models.DecimalField(decimal_places=5, default=1, help_text='Item quantity required for repair', max_digits=15, verbose_name='Quantity')),
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lines', to='order.repairorder', verbose_name='Repair Order')),
('part', models.ForeignKey(blank=True, help_text='Part to be consumed for repair', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='repair_order_line_items', to='part.part', verbose_name='Part')),
],
options={
'verbose_name': 'Repair Order Line Item',
},
bases=(InvenTree.models.ContentTypeMixin, InvenTree.models.PluginValidationMixin, models.Model),
),
migrations.CreateModel(
name='RepairOrderAllocation',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.DecimalField(decimal_places=5, default=1, help_text='Allocated stock quantity', max_digits=15, verbose_name='Quantity')),
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='repair_order_allocations', to='stock.stockitem', verbose_name='Stock Item')),
('line', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='allocations', to='order.repairorderlineitem', verbose_name='Line Item')),
],
options={
'verbose_name': 'Repair Order Allocation',
},
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Generated by Django 5.2.14 on 2026-06-04 11:07

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('order', '0120_purchaseorder_tags_returnorder_tags_salesorder_tags_and_more'),
('order', '0120_repairorder_repairorderlineitem_and_more'),
]

operations = [
]
143 changes: 143 additions & 0 deletions src/backend/InvenTree/order/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
from order.status_codes import (
PurchaseOrderStatus,
PurchaseOrderStatusGroups,
RepairOrderStatus,
ReturnOrderLineStatus,
ReturnOrderStatus,
ReturnOrderStatusGroups,
Expand Down Expand Up @@ -4002,3 +4003,145 @@ def _touch_order_updated_at(instance):
def update_order_on_lineitem_change(sender, instance, **kwargs):
"""Update parent order updated_at when any line item is saved or deleted."""
_touch_order_updated_at(instance)


class RepairOrder(
StatusCodeMixin,
StateTransitionMixin,
InvenTree.models.InvenTreeParameterMixin,
InvenTree.models.InvenTreeAttachmentMixin,
InvenTree.models.InvenTreeBarcodeMixin,
InvenTree.models.InvenTreeNotesMixin,
report.mixins.InvenTreeReportMixin,
InvenTree.models.ReferenceIndexingMixin,
InvenTree.models.InvenTreeMetadataModel,
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.

You will need to add a number of other mixin classes here - look at the "BuildOrder" model for reference:

class Build(
    InvenTree.models.PluginValidationMixin,
    report.mixins.InvenTreeReportMixin,
    InvenTree.models.InvenTreeParameterMixin,
    InvenTree.models.InvenTreeAttachmentMixin,
    InvenTree.models.InvenTreeBarcodeMixin,
    InvenTree.models.InvenTreeNotesMixin,
    InvenTree.models.ReferenceIndexingMixin,
    StateTransitionMixin,
    StatusCodeMixin,
    InvenTree.models.MetadataMixin,
    InvenTree.models.InvenTreeTree,
):

):
"""A RepairOrder represents a repair request from a customer."""

STATUS_CLASS = RepairOrderStatus
REFERENCE_PATTERN_SETTING = 'REPAIRORDER_REFERENCE_PATTERN'

class Meta:
"""Model meta options."""

verbose_name = _('Repair Order')

reference = models.CharField(
max_length=100,
unique=True,
blank=False,
null=False,
help_text=_('Repair Order Reference'),
verbose_name=_('Reference'),
)

customer = models.ForeignKey(
'company.Company',
on_delete=models.SET_NULL,
blank=True,
null=True,
related_name='repair_orders',
limit_choices_to={'is_customer': True},
verbose_name=_('Customer'),
help_text=_('Customer reference'),
)

description = models.CharField(
max_length=250,
verbose_name=_('Description'),
help_text=_('Repair order description'),
)

symptoms = models.TextField(
blank=True,
verbose_name=_('Symptoms'),
help_text=_('Reported symptoms or issues'),
)

status = InvenTreeCustomStatusModelField(
default=RepairOrderStatus.PENDING.value,
choices=RepairOrderStatus.items(),
status_class=RepairOrderStatus,
help_text=_('Repair order status'),
verbose_name=_('Status'),
)

@staticmethod
def get_api_url():
"""Return the API URL associated with the RepairOrder model."""
return reverse('api-repair-order-list')


class RepairOrderLineItem(InvenTree.models.InvenTreeMetadataModel):
"""Model for a repair order line item."""

class Meta:
"""Model meta options."""

verbose_name = _('Repair Order Line Item')

order = models.ForeignKey(
RepairOrder,
on_delete=models.CASCADE,
related_name='lines',
verbose_name=_('Repair Order'),
)

part = models.ForeignKey(
'part.Part',
on_delete=models.SET_NULL,
blank=True,
null=True,
related_name='repair_order_line_items',
verbose_name=_('Part'),
help_text=_('Part to be consumed for repair'),
)

quantity = models.DecimalField(
max_digits=15,
decimal_places=5,
default=1,
verbose_name=_('Quantity'),
help_text=_('Item quantity required for repair'),
)

@staticmethod
def get_api_url():
"""Return the API URL associated with the RepairOrderLineItem model."""
return reverse('api-repair-order-line-list')


class RepairOrderAllocation(models.Model):
"""Model linking RepairOrderLineItem to specific stock.StockItem quantities."""

class Meta:
"""Model meta options."""

verbose_name = _('Repair Order Allocation')

line = models.ForeignKey(
RepairOrderLineItem,
on_delete=models.CASCADE,
related_name='allocations',
verbose_name=_('Line Item'),
)

item = models.ForeignKey(
'stock.StockItem',
on_delete=models.CASCADE,
related_name='repair_order_allocations',
verbose_name=_('Stock Item'),
)

quantity = models.DecimalField(
max_digits=15,
decimal_places=5,
default=1,
verbose_name=_('Quantity'),
help_text=_('Allocated stock quantity'),
)

@staticmethod
def get_api_url():
"""Return the API URL associated with the RepairOrderAllocation model."""
return reverse('api-repair-order-allocation-list')
Loading
Loading