diff --git a/src/backend/InvenTree/generic/states/tests.py b/src/backend/InvenTree/generic/states/tests.py index fe33eaf78e02..a7dcfb2cfc47 100644 --- a/src/backend/InvenTree/generic/states/tests.py +++ b/src/backend/InvenTree/generic/states/tests.py @@ -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'] @@ -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') diff --git a/src/backend/InvenTree/order/admin.py b/src/backend/InvenTree/order/admin.py index 875ebdb763fa..aac9ee62a553 100644 --- a/src/backend/InvenTree/order/admin.py +++ b/src/backend/InvenTree/order/admin.py @@ -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'] diff --git a/src/backend/InvenTree/order/api.py b/src/backend/InvenTree/order/api.py index dbe2127a33cb..cff3d35d2a39 100644 --- a/src/backend/InvenTree/order/api.py +++ b/src/backend/InvenTree/order/api.py @@ -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 + + +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( + '/', + 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( + '/', + 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( + '/', + 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( @@ -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)), ] diff --git a/src/backend/InvenTree/order/migrations/0120_repairorder_repairorderlineitem_and_more.py b/src/backend/InvenTree/order/migrations/0120_repairorder_repairorderlineitem_and_more.py new file mode 100644 index 000000000000..591c323a1a3d --- /dev/null +++ b/src/backend/InvenTree/order/migrations/0120_repairorder_repairorderlineitem_and_more.py @@ -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', + }, + ), + ] diff --git a/src/backend/InvenTree/order/migrations/0121_merge_20260604_1637.py b/src/backend/InvenTree/order/migrations/0121_merge_20260604_1637.py new file mode 100644 index 000000000000..5d107e02b544 --- /dev/null +++ b/src/backend/InvenTree/order/migrations/0121_merge_20260604_1637.py @@ -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 = [ + ] diff --git a/src/backend/InvenTree/order/models.py b/src/backend/InvenTree/order/models.py index 1109e65a021a..7b008d57a9b6 100644 --- a/src/backend/InvenTree/order/models.py +++ b/src/backend/InvenTree/order/models.py @@ -54,6 +54,7 @@ from order.status_codes import ( PurchaseOrderStatus, PurchaseOrderStatusGroups, + RepairOrderStatus, ReturnOrderLineStatus, ReturnOrderStatus, ReturnOrderStatusGroups, @@ -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, +): + """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') diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py index 108a6d51c147..8656f93288d4 100644 --- a/src/backend/InvenTree/order/serializers.py +++ b/src/backend/InvenTree/order/serializers.py @@ -3039,3 +3039,33 @@ def save(self): with transaction.atomic(): order.models.TransferOrderAllocation.objects.bulk_create(allocations) + + +class RepairOrderSerializer(NotesFieldMixin, InvenTreeModelSerializer): + """Serializer for a RepairOrder object.""" + + class Meta: + """Metaclass options.""" + + model = order.models.RepairOrder + fields = ['pk', 'reference', 'customer', 'description', 'symptoms', 'status'] + + +class RepairOrderLineItemSerializer(InvenTreeModelSerializer): + """Serializer for a RepairOrderLineItem object.""" + + class Meta: + """Metaclass options.""" + + model = order.models.RepairOrderLineItem + fields = ['pk', 'order', 'part', 'quantity'] + + +class RepairOrderAllocationSerializer(InvenTreeModelSerializer): + """Serializer for a RepairOrderAllocation object.""" + + class Meta: + """Metaclass options.""" + + model = order.models.RepairOrderAllocation + fields = ['pk', 'line', 'item', 'quantity'] diff --git a/src/backend/InvenTree/order/status_codes.py b/src/backend/InvenTree/order/status_codes.py index d8893a1fa3c8..a248e49a446b 100644 --- a/src/backend/InvenTree/order/status_codes.py +++ b/src/backend/InvenTree/order/status_codes.py @@ -142,3 +142,26 @@ class TransferOrderStatusGroups: FAILED = [TransferOrderStatus.CANCELLED.value] COMPLETE = [TransferOrderStatus.COMPLETE.value] + + +class RepairOrderStatus(StatusCode): + """Defines a set of status codes for a RepairOrder.""" + + PENDING = 10, _('Pending'), ColorEnum.secondary # Repair is pending + IN_PROGRESS = 20, _('In Progress'), ColorEnum.primary # Repair is underway + ON_HOLD = 25, _('On Hold'), ColorEnum.warning # Repair is on hold + COMPLETE = 30, _('Complete'), ColorEnum.success # Repair has been completed + CANCELLED = 40, _('Cancelled'), ColorEnum.danger # Repair was cancelled + + +class RepairOrderStatusGroups: + """Groups for RepairOrderStatus codes.""" + + # Open orders + OPEN = [ + RepairOrderStatus.PENDING.value, + RepairOrderStatus.ON_HOLD.value, + RepairOrderStatus.IN_PROGRESS.value, + ] + + COMPLETE = [RepairOrderStatus.COMPLETE.value] diff --git a/src/backend/InvenTree/users/oauth2_scopes.py b/src/backend/InvenTree/users/oauth2_scopes.py index 52656a33fa94..d52381440bbe 100644 --- a/src/backend/InvenTree/users/oauth2_scopes.py +++ b/src/backend/InvenTree/users/oauth2_scopes.py @@ -21,6 +21,7 @@ def get_granular_scope(method, role=None, type='r'): 'sales_order': 'Role Sales Orders', 'return_order': 'Role Return Orders', 'transfer_order': 'Role Transfer Orders', + 'repair_order': 'Role Repair Orders', } _methods = {'view': 'GET', 'add': 'POST', 'change': 'PUT / PATCH', 'delete': 'DELETE'} diff --git a/src/backend/InvenTree/users/ruleset.py b/src/backend/InvenTree/users/ruleset.py index 3787eb89b9b7..adb20dccd692 100644 --- a/src/backend/InvenTree/users/ruleset.py +++ b/src/backend/InvenTree/users/ruleset.py @@ -20,6 +20,7 @@ class RuleSetEnum(StringEnum): SALES_ORDER = 'sales_order' RETURN_ORDER = 'return_order' TRANSFER_ORDER = 'transfer_order' + REPAIR_ORDER = 'repair_order' # This is a list of all the ruleset choices available in the system. @@ -36,6 +37,7 @@ class RuleSetEnum(StringEnum): (RuleSetEnum.SALES_ORDER, _('Sales Orders')), (RuleSetEnum.RETURN_ORDER, _('Return Orders')), (RuleSetEnum.TRANSFER_ORDER, _('Transfer Orders')), + (RuleSetEnum.REPAIR_ORDER, _('Repair Orders')), ] # Ruleset names available in the system. @@ -168,6 +170,11 @@ def get_ruleset_models() -> dict: 'order_transferorderallocation', 'order_transferorderlineitem', ], + RuleSetEnum.REPAIR_ORDER: [ + 'order_repairorder', + 'order_repairorderlineitem', + 'order_repairorderallocation', + ], } if settings.SITE_MULTI: diff --git a/src/frontend/lib/enums/ApiEndpoints.tsx b/src/frontend/lib/enums/ApiEndpoints.tsx index 972b45e60c6b..43d830b0dcf7 100644 --- a/src/frontend/lib/enums/ApiEndpoints.tsx +++ b/src/frontend/lib/enums/ApiEndpoints.tsx @@ -200,6 +200,14 @@ export enum ApiEndpoints { return_order_line_list = 'order/ro-line/', return_order_extra_line_list = 'order/ro-extra-line/', + repair_order_list = 'order/repair/', + repair_order_issue = 'order/repair/:id/issue/', + repair_order_hold = 'order/repair/:id/hold/', + repair_order_cancel = 'order/repair/:id/cancel/', + repair_order_complete = 'order/repair/:id/complete/', + repair_order_line_list = 'order/repair-line/', + repair_order_allocation_list = 'order/repair-allocation/', + transfer_order_list = 'order/transfer-order/', transfer_order_issue = 'order/transfer-order/:id/issue/', transfer_order_hold = 'order/transfer-order/:id/hold/', diff --git a/src/frontend/lib/enums/ModelInformation.tsx b/src/frontend/lib/enums/ModelInformation.tsx index 5989e0acf914..258eae5168a2 100644 --- a/src/frontend/lib/enums/ModelInformation.tsx +++ b/src/frontend/lib/enums/ModelInformation.tsx @@ -208,6 +208,28 @@ export const ModelInformationDict: ModelDict = { api_endpoint: ApiEndpoints.return_order_line_list, icon: 'return_orders' }, + repairorder: { + label: () => t`Repair Order`, + label_multiple: () => t`Repair Orders`, + url_overview: '/sales/index/repairorders', + url_detail: '/sales/repair-order/:pk/', + api_endpoint: ApiEndpoints.repair_order_list, + admin_url: '/order/repairorder/', + supports_barcode: true, + icon: 'repair_orders' + }, + repairorderlineitem: { + label: () => t`Repair Order Line Item`, + label_multiple: () => t`Repair Order Line Items`, + api_endpoint: ApiEndpoints.repair_order_line_list, + icon: 'repair_orders' + }, + repairorderallocation: { + label: () => t`Repair Order Allocation`, + label_multiple: () => t`Repair Order Allocations`, + api_endpoint: ApiEndpoints.repair_order_allocation_list, + icon: 'repair_orders' + }, transferorder: { label: () => t`Transfer Order`, label_multiple: () => t`Transfer Orders`, diff --git a/src/frontend/lib/enums/ModelType.tsx b/src/frontend/lib/enums/ModelType.tsx index 76642756e281..907a5da42aed 100644 --- a/src/frontend/lib/enums/ModelType.tsx +++ b/src/frontend/lib/enums/ModelType.tsx @@ -22,6 +22,9 @@ export enum ModelType { purchaseorderlineitem = 'purchaseorderlineitem', salesorder = 'salesorder', salesordershipment = 'salesordershipment', + repairorder = 'repairorder', + repairorderlineitem = 'repairorderlineitem', + repairorderallocation = 'repairorderallocation', returnorder = 'returnorder', returnorderlineitem = 'returnorderlineitem', transferorder = 'transferorder', diff --git a/src/frontend/lib/enums/Roles.tsx b/src/frontend/lib/enums/Roles.tsx index 3d7ff9c78b2c..f575c00f47cb 100644 --- a/src/frontend/lib/enums/Roles.tsx +++ b/src/frontend/lib/enums/Roles.tsx @@ -12,6 +12,7 @@ export enum UserRoles { purchase_order = 'purchase_order', return_order = 'return_order', transfer_order = 'transfer_order', + repair_order = 'repair_order', sales_order = 'sales_order', stock = 'stock', stock_location = 'stock_location' @@ -43,6 +44,8 @@ export function userRoleLabel(role: UserRoles): string { return t`Return Orders`; case UserRoles.transfer_order: return t`Transfer Orders`; + case UserRoles.repair_order: + return t`Repair Orders`; case UserRoles.sales_order: return t`Sales Orders`; case UserRoles.stock: diff --git a/src/frontend/lib/index.ts b/src/frontend/lib/index.ts index 0dfb4f474294..3642e341baf9 100644 --- a/src/frontend/lib/index.ts +++ b/src/frontend/lib/index.ts @@ -110,7 +110,6 @@ export { ProgressBar } from './components/ProgressBar'; export { PassFailButton, YesNoButton } from './components/YesNoButton'; export { SearchInput } from './components/SearchInput'; export { TableColumnSelect } from './components/TableColumnSelect'; -export { default as TagsList } from './components/TagsList'; export { default as InvenTreeTable } from './components/InvenTreeTable'; export { RowViewAction, diff --git a/src/frontend/lib/types/Filters.tsx b/src/frontend/lib/types/Filters.tsx index 1fe7806388f9..32f4b7540bd7 100644 --- a/src/frontend/lib/types/Filters.tsx +++ b/src/frontend/lib/types/Filters.tsx @@ -41,7 +41,6 @@ export type TableFilter = { name: string; label?: string; description?: string; - placeholder?: string; type?: TableFilterType; choices?: TableFilterChoice[]; choiceFunction?: () => TableFilterChoice[]; @@ -53,8 +52,6 @@ export type TableFilter = { apiFilter?: Record; model?: ModelType; modelRenderer?: (instance: any) => string; - transform?: (item: any) => TableFilterChoice; - multi?: boolean; }; /* diff --git a/src/frontend/playwright/global-setup.ts b/src/frontend/playwright/global-setup.ts index 4930c98b2346..2bdcca35d161 100644 --- a/src/frontend/playwright/global-setup.ts +++ b/src/frontend/playwright/global-setup.ts @@ -1,7 +1,6 @@ -import { type FullConfig, chromium, request } from '@playwright/test'; - import fs from 'node:fs'; import path from 'node:path'; +import { type FullConfig, chromium, request } from '@playwright/test'; import { adminuser, allaccessuser, diff --git a/src/frontend/src/components/details/Details.tsx b/src/frontend/src/components/details/Details.tsx index d221ea543d50..f1ae66f79b0b 100644 --- a/src/frontend/src/components/details/Details.tsx +++ b/src/frontend/src/components/details/Details.tsx @@ -407,7 +407,7 @@ function TableAnchorValue(props: Readonly) { let make_link = props.field_data?.link ?? true; // Construct the "return value" for the fetched data - let value = undefined; + let value: any; if (props.field_data.model_formatter) { value = props.field_data.model_formatter(data) ?? value; diff --git a/src/frontend/src/components/forms/fields/ApiFormField.tsx b/src/frontend/src/components/forms/fields/ApiFormField.tsx index adffd6063e66..32acc3bdad6a 100644 --- a/src/frontend/src/components/forms/fields/ApiFormField.tsx +++ b/src/frontend/src/components/forms/fields/ApiFormField.tsx @@ -17,7 +17,6 @@ import { NestedObjectField } from './NestedObjectField'; import NumberField from './NumberField'; import { RelatedModelField } from './RelatedModelField'; import { TableField } from './TableField'; -import TagsField from './TagsField'; import TextField from './TextField'; /** @@ -250,10 +249,6 @@ export function ApiFormField({ control={controller} /> ); - case 'tags': - return ( - - ); default: return ( diff --git a/src/frontend/src/components/forms/fields/BooleanField.tsx b/src/frontend/src/components/forms/fields/BooleanField.tsx index 033e58cf3045..475c7628e573 100644 --- a/src/frontend/src/components/forms/fields/BooleanField.tsx +++ b/src/frontend/src/components/forms/fields/BooleanField.tsx @@ -34,13 +34,13 @@ export function BooleanField({ // Coerce the value to a (stringified) boolean value const booleanValue: boolean = useMemo(() => { - return isTrue(value ?? definition.default ?? false); + return isTrue(value); }, [value]); return ( +): ReactNode { + const { instance } = props; + const customer = instance?.customer_detail || {}; + + return ( + + ); +} + +export function RenderRepairOrderLineItem( + props: Readonly +): ReactNode { + const { instance } = props; + + return ( + + ); +} + +export function RenderRepairOrderAllocation({ + instance +}: Readonly<{ + instance: any; +}>): ReactNode { + const order = instance.order_detail || {}; + + return ( + {`${t`Allocation`} ${instance.pk}`}} + /> + ); +} + /** * Inline rendering of a single SalesOrder instance */ diff --git a/src/frontend/src/defaults/actions.tsx b/src/frontend/src/defaults/actions.tsx index fb0af4ef78c7..6bab43c5ccc2 100644 --- a/src/frontend/src/defaults/actions.tsx +++ b/src/frontend/src/defaults/actions.tsx @@ -146,6 +146,17 @@ export function getActions(navigate: NavigateFunction) { leftSection: }); + globalSettings.isSet('REPAIRORDER_ENABLED') && + user?.hasViewRole(UserRoles.repair_order) && + _actions.push({ + id: 'repair-orders', + label: t`Repair Orders`, + description: t`Go to Repair Orders`, + onClick: () => + navigate(ModelInformationDict['repairorder'].url_overview!), + leftSection: + }); + globalSettings.isSet('BARCODE_ENABLE') && _actions.push({ id: 'scan', diff --git a/src/frontend/src/defaults/backendMappings.tsx b/src/frontend/src/defaults/backendMappings.tsx index 058ad80dbf1a..49040e246c2c 100644 --- a/src/frontend/src/defaults/backendMappings.tsx +++ b/src/frontend/src/defaults/backendMappings.tsx @@ -11,6 +11,8 @@ export const statusCodeList: Record = { PurchaseOrderStatus: ModelType.purchaseorder, ReturnOrderStatus: ModelType.returnorder, ReturnOrderLineStatus: ModelType.returnorderlineitem, + RepairOrderStatus: ModelType.repairorder, + RepairOrderLineStatus: ModelType.repairorderlineitem, TransferOrderStatus: ModelType.transferorder, TransferOrderLineStatus: ModelType.transferorderlineitem, SalesOrderStatus: ModelType.salesorder, diff --git a/src/frontend/src/defaults/links.tsx b/src/frontend/src/defaults/links.tsx index caae4d34c654..587403aef01e 100644 --- a/src/frontend/src/defaults/links.tsx +++ b/src/frontend/src/defaults/links.tsx @@ -71,7 +71,9 @@ export function getNavTabs(user: UserStateProps): NavTab[] { visible: user.hasViewRole(UserRoles.sales_order) || (globalSettings.isSet('RETURNORDER_ENABLED') && - user.hasViewRole(UserRoles.return_order)) + user.hasViewRole(UserRoles.return_order)) || + (globalSettings.isSet('REPAIRORDER_ENABLED') && + user.hasViewRole(UserRoles.repair_order)) } ]; diff --git a/src/frontend/src/forms/BuildForms.tsx b/src/frontend/src/forms/BuildForms.tsx index 3fd3acbc41c1..d9237b206ed5 100644 --- a/src/frontend/src/forms/BuildForms.tsx +++ b/src/frontend/src/forms/BuildForms.tsx @@ -36,7 +36,6 @@ import { } from '../hooks/UseGenerator'; import { useGlobalSettingsState } from '../states/SettingsStates'; import { RenderPartColumn } from '../tables/ColumnRenderers'; -import { TagsField } from './CommonFields'; /** * Field set for BuildOrder forms @@ -125,7 +124,6 @@ export function useBuildOrderFields({ }, value: destination }, - tags: TagsField({}), link: { icon: }, diff --git a/src/frontend/src/forms/CompanyForms.tsx b/src/frontend/src/forms/CompanyForms.tsx index 14d3b6218f88..9cefd6147f9a 100644 --- a/src/frontend/src/forms/CompanyForms.tsx +++ b/src/frontend/src/forms/CompanyForms.tsx @@ -13,7 +13,6 @@ import { IconPhone } from '@tabler/icons-react'; import { useMemo, useState } from 'react'; -import { TagsField } from './CommonFields'; /** * Field set for SupplierPart instance @@ -83,7 +82,6 @@ export function useSupplierPartFields({ icon: }, description: {}, - tags: TagsField({}), link: { icon: }, @@ -119,7 +117,6 @@ export function useManufacturerPartFields() { }, MPN: {}, description: {}, - tags: TagsField({}), link: {} }; @@ -146,7 +143,6 @@ export function companyFields(): ApiFormFieldSet { email: { icon: }, - tags: TagsField({}), tax_id: {}, is_supplier: {}, is_manufacturer: {}, diff --git a/src/frontend/src/forms/PartForms.tsx b/src/frontend/src/forms/PartForms.tsx index 311b64b18f94..35836e94ab12 100644 --- a/src/frontend/src/forms/PartForms.tsx +++ b/src/frontend/src/forms/PartForms.tsx @@ -3,7 +3,6 @@ import { t } from '@lingui/core/macro'; import { IconBuildingStore, IconCopy, IconPackages } from '@tabler/icons-react'; import { useEffect, useMemo, useState } from 'react'; import { useGlobalSettingsState } from '../states/SettingsStates'; -import { TagsField } from './CommonFields'; /** * Construct a set of fields for creating / editing a Part instance @@ -55,7 +54,6 @@ export function usePartFields({ } }, keywords: {}, - tags: TagsField({}), units: {}, link: {}, default_location: { diff --git a/src/frontend/src/forms/PurchaseOrderForms.tsx b/src/frontend/src/forms/PurchaseOrderForms.tsx index 4faf5756ad23..d12916a49bec 100644 --- a/src/frontend/src/forms/PurchaseOrderForms.tsx +++ b/src/frontend/src/forms/PurchaseOrderForms.tsx @@ -57,7 +57,6 @@ import { useSerialNumberGenerator } from '../hooks/UseGenerator'; import { useGlobalSettingsState } from '../states/SettingsStates'; -import { TagsField } from './CommonFields'; /* * Construct a set of fields for creating / editing a PurchaseOrderLineItem instance */ @@ -288,7 +287,6 @@ export function usePurchaseOrderFields({ structural: false } }, - tags: TagsField({}), link: {}, contact: { icon: , diff --git a/src/frontend/src/forms/RepairOrderForms.tsx b/src/frontend/src/forms/RepairOrderForms.tsx new file mode 100644 index 000000000000..315ea2110193 --- /dev/null +++ b/src/frontend/src/forms/RepairOrderForms.tsx @@ -0,0 +1,69 @@ +import { useMemo } from 'react'; + +import type { ApiFormFieldSet } from '@lib/types/Forms'; + +export function useRepairOrderFields({ + duplicateOrderId +}: { + duplicateOrderId?: number; +}): ApiFormFieldSet { + return useMemo(() => { + const fields: ApiFormFieldSet = { + reference: {}, + description: {}, + customer: { + disabled: duplicateOrderId != undefined, + filters: { + is_customer: true, + active: true + } + }, + asset: { + filters: { + is_building: false + } + }, + symptoms: {} + }; + + // Order duplication fields + if (!!duplicateOrderId) { + fields.duplicate = { + children: { + order_id: { + hidden: true, + value: duplicateOrderId + }, + copy_lines: { + value: true + } + } + }; + } + + return fields; + }, [duplicateOrderId]); +} + +export function useRepairOrderLineItemFields({ + orderId, + create +}: { + orderId: number; + create?: boolean; +}) { + return useMemo(() => { + return { + order: { + disabled: true, + value: orderId + }, + part: { + filters: { + active: true + } + }, + quantity: {} + }; + }, [create, orderId]); +} diff --git a/src/frontend/src/forms/ReturnOrderForms.tsx b/src/frontend/src/forms/ReturnOrderForms.tsx index ea919f5da72f..2f5be88aa88d 100644 --- a/src/frontend/src/forms/ReturnOrderForms.tsx +++ b/src/frontend/src/forms/ReturnOrderForms.tsx @@ -23,7 +23,6 @@ import { Thumbnail } from '../components/images/Thumbnail'; import { useCreateApiFormModal } from '../hooks/UseForm'; import { useGlobalSettingsState } from '../states/SettingsStates'; import { StatusFilterOptions } from '../tables/Filter'; -import { TagsField } from './CommonFields'; export function useReturnOrderFields({ duplicateOrderId @@ -53,7 +52,6 @@ export function useReturnOrderFields({ icon: }, link: {}, - tags: TagsField({}), contact: { icon: , adjustFilters: (value: ApiFormAdjustFilterType) => { diff --git a/src/frontend/src/forms/SalesOrderForms.tsx b/src/frontend/src/forms/SalesOrderForms.tsx index 125ca7f89495..ecfe82993c4f 100644 --- a/src/frontend/src/forms/SalesOrderForms.tsx +++ b/src/frontend/src/forms/SalesOrderForms.tsx @@ -31,7 +31,6 @@ import { useCreateApiFormModal, useEditApiFormModal } from '../hooks/UseForm'; import { useGlobalSettingsState } from '../states/SettingsStates'; import { useUserState } from '../states/UserState'; import { RenderPartColumn } from '../tables/ColumnRenderers'; -import { TagsField } from './CommonFields'; export function useSalesOrderFields({ duplicateOrderId @@ -65,7 +64,6 @@ export function useSalesOrderFields({ target_date: { icon: }, - tags: TagsField({}), link: {}, contact: { icon: , @@ -539,7 +537,6 @@ export function useSalesOrderShipmentFields({ }, tracking_number: {}, invoice_number: {}, - tags: TagsField({}), link: {} }; }, [customerId, pending]); diff --git a/src/frontend/src/forms/StockForms.tsx b/src/frontend/src/forms/StockForms.tsx index 99255cf0d5e5..fe209f11c9d1 100644 --- a/src/frontend/src/forms/StockForms.tsx +++ b/src/frontend/src/forms/StockForms.tsx @@ -61,7 +61,6 @@ import { import useStatusCodes from '../hooks/UseStatusCodes'; import { useGlobalSettingsState } from '../states/SettingsStates'; import { StatusFilterOptions } from '../tables/Filter'; -import { TagsField } from './CommonFields'; /** * Construct a set of fields for creating / editing a StockItem instance @@ -273,7 +272,6 @@ export function useStockFields({ packaging: { icon: }, - tags: TagsField({}), link: { icon: }, diff --git a/src/frontend/src/forms/TransferOrderForms.tsx b/src/frontend/src/forms/TransferOrderForms.tsx index 3935d89b923c..a7e3c8be3f50 100644 --- a/src/frontend/src/forms/TransferOrderForms.tsx +++ b/src/frontend/src/forms/TransferOrderForms.tsx @@ -10,7 +10,6 @@ import type { TableFieldRowProps } from '../components/forms/fields/TableField'; import { useCreateApiFormModal } from '../hooks/UseForm'; import { useGlobalSettingsState } from '../states/SettingsStates'; import { RenderPartColumn } from '../tables/ColumnRenderers'; -import { TagsField } from './CommonFields'; export function useTransferOrderFields({ duplicateOrderId @@ -37,7 +36,6 @@ export function useTransferOrderFields({ } }, consume: {}, - tags: TagsField({}), link: {}, responsible: { filters: { diff --git a/src/frontend/src/functions/icons.tsx b/src/frontend/src/functions/icons.tsx index bdc1826e8947..af460da4398a 100644 --- a/src/frontend/src/functions/icons.tsx +++ b/src/frontend/src/functions/icons.tsx @@ -159,6 +159,7 @@ const icons: InvenTreeIconType = { customers: IconBuildingStore, purchase_orders: IconShoppingCart, return_orders: IconTruckReturn, + repair_orders: IconTool, transfer_orders: IconTransfer, sales_orders: IconTruckDelivery, scheduling: IconCalendarStats, diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx index f249332eb991..7460634ccbf9 100644 --- a/src/frontend/src/pages/build/BuildDetail.tsx +++ b/src/frontend/src/pages/build/BuildDetail.tsx @@ -21,7 +21,6 @@ import { ModelType } from '@lib/enums/ModelType'; import { UserRoles } from '@lib/enums/Roles'; import { apiUrl } from '@lib/functions/Api'; import { getDetailUrl } from '@lib/functions/Navigation'; -import { TagsList } from '@lib/index'; import type { ApiFormFieldSet } from '@lib/types/Forms'; import type { PanelType } from '@lib/types/Panel'; import AdminButton from '../../components/buttons/AdminButton'; @@ -229,8 +228,7 @@ export default function BuildDetail() { endpoint: ApiEndpoints.build_order_list, pk: id, params: { - part_detail: true, - tags: true + part_detail: true }, refetchOnMount: true }); @@ -440,20 +438,17 @@ export default function BuildDetail() { return ( - - - - - - - - - + + + + + + @@ -617,7 +612,6 @@ export default function BuildDetail() { title: t`Edit Build Order`, modalId: 'edit-build-order', fields: editBuildOrderFields, - queryParams: new URLSearchParams({ tags: 'true' }), onFormSuccess: refreshInstance }); diff --git a/src/frontend/src/pages/company/CompanyDetail.tsx b/src/frontend/src/pages/company/CompanyDetail.tsx index 2b779d41122f..b23b8c19fd72 100644 --- a/src/frontend/src/pages/company/CompanyDetail.tsx +++ b/src/frontend/src/pages/company/CompanyDetail.tsx @@ -18,7 +18,6 @@ import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import { ModelType } from '@lib/enums/ModelType'; import { UserRoles } from '@lib/enums/Roles'; import { apiUrl } from '@lib/functions/Api'; -import { TagsList } from '@lib/index'; import type { PanelType } from '@lib/types/Panel'; import AdminButton from '../../components/buttons/AdminButton'; import { PrintingActions } from '../../components/buttons/PrintingActions'; @@ -79,9 +78,7 @@ export default function CompanyDetail(props: Readonly) { } = useInstance({ endpoint: ApiEndpoints.company_list, pk: id, - params: { - tags: true - }, + params: {}, refetchOnMount: true }); @@ -156,26 +153,23 @@ export default function CompanyDetail(props: Readonly) { return ( - - - - - - - - - + + + + + + ); @@ -294,7 +288,6 @@ export default function CompanyDetail(props: Readonly) { pk: company?.pk, title: t`Edit Company`, fields: companyFields(), - queryParams: new URLSearchParams({ tags: 'true' }), onFormSuccess: refreshInstance }); diff --git a/src/frontend/src/pages/company/ManufacturerPartDetail.tsx b/src/frontend/src/pages/company/ManufacturerPartDetail.tsx index 33b00ba4703e..66e1a1a347d6 100644 --- a/src/frontend/src/pages/company/ManufacturerPartDetail.tsx +++ b/src/frontend/src/pages/company/ManufacturerPartDetail.tsx @@ -8,7 +8,6 @@ import { import { useMemo } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; -import TagsList from '@lib/components/TagsList'; import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import { ModelType } from '@lib/enums/ModelType'; import { UserRoles } from '@lib/enums/Roles'; @@ -60,8 +59,7 @@ export default function ManufacturerPartDetail() { hasPrimaryKey: true, params: { part_detail: true, - manufacturer_detail: true, - tags: true + manufacturer_detail: true } }); @@ -135,23 +133,20 @@ export default function ManufacturerPartDetail() { return ( - - - - - - - - - + + + + + + ); @@ -216,7 +211,6 @@ export default function ManufacturerPartDetail() { pk: manufacturerPart?.pk, title: t`Edit Manufacturer Part`, fields: editManufacturerPartFields, - queryParams: new URLSearchParams({ tags: 'true' }), onFormSuccess: refreshInstance }); diff --git a/src/frontend/src/pages/company/SupplierPartDetail.tsx b/src/frontend/src/pages/company/SupplierPartDetail.tsx index 4f16295460cf..c61f4eaf3267 100644 --- a/src/frontend/src/pages/company/SupplierPartDetail.tsx +++ b/src/frontend/src/pages/company/SupplierPartDetail.tsx @@ -9,7 +9,6 @@ import { import { type ReactNode, useMemo } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; -import TagsList from '@lib/components/TagsList'; import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import { ModelType } from '@lib/enums/ModelType'; import { UserRoles } from '@lib/enums/Roles'; @@ -68,8 +67,7 @@ export default function SupplierPartDetail() { params: { part_detail: true, supplier_detail: true, - manufacturer_detail: true, - tags: true + manufacturer_detail: true } }); @@ -223,23 +221,20 @@ export default function SupplierPartDetail() { return ( - - - - - - - - - + + + + + + @@ -344,7 +339,6 @@ export default function SupplierPartDetail() { pk: supplierPart?.pk, title: t`Edit Supplier Part`, fields: supplierPartFields, - queryParams: new URLSearchParams({ tags: 'true' }), onFormSuccess: refreshInstance }); diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index 5182a1c249d3..c53588a4a15c 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -41,7 +41,6 @@ import { type ReactNode, useMemo, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import Select from 'react-select'; -import TagsList from '@lib/components/TagsList'; import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import { ModelType } from '@lib/enums/ModelType'; import { UserRoles } from '@lib/enums/Roles'; @@ -191,8 +190,7 @@ export default function PartDetail() { endpoint: ApiEndpoints.part_list, pk: id, params: { - path_detail: true, - tags: true + path_detail: true }, refetchOnMount: true }); @@ -614,7 +612,6 @@ export default function PartDetail() { - {enableRevisionSelection && ( @@ -1001,7 +998,6 @@ export default function PartDetail() { pk: part.pk, title: t`Edit Part`, fields: partFields, - queryParams: new URLSearchParams({ tags: 'true' }), onFormSuccess: refreshInstance }); diff --git a/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx b/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx index 99099bad1ac2..db4fa6107fbf 100644 --- a/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx +++ b/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx @@ -9,7 +9,6 @@ import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import { ModelType } from '@lib/enums/ModelType'; import { UserRoles } from '@lib/enums/Roles'; import { apiUrl } from '@lib/functions/Api'; -import { TagsList } from '@lib/index'; import type { PanelType } from '@lib/types/Panel'; import AdminButton from '../../components/buttons/AdminButton'; import PrimaryActionButton from '../../components/buttons/PrimaryActionButton'; @@ -66,8 +65,7 @@ export default function PurchaseOrderDetail() { endpoint: ApiEndpoints.purchase_order_list, pk: id, params: { - supplier_detail: true, - tags: true + supplier_detail: true }, refetchOnMount: true }); @@ -91,7 +89,6 @@ export default function PurchaseOrderDetail() { pk: id, title: t`Edit Purchase Order`, fields: purchaseOrderFields, - queryParams: new URLSearchParams({ tags: 'true' }), onFormSuccess: () => { refreshInstance(); } @@ -321,20 +318,17 @@ export default function PurchaseOrderDetail() { return ( - - - - - - - - - + + + + + + diff --git a/src/frontend/src/pages/sales/RepairOrderDetail.tsx b/src/frontend/src/pages/sales/RepairOrderDetail.tsx new file mode 100644 index 000000000000..f77f8c5ce2d8 --- /dev/null +++ b/src/frontend/src/pages/sales/RepairOrderDetail.tsx @@ -0,0 +1,406 @@ +import { t } from '@lingui/core/macro'; +import { Accordion, Grid, Skeleton, Stack } from '@mantine/core'; +import { IconInfoCircle, IconList } from '@tabler/icons-react'; +import { type ReactNode, useMemo } from 'react'; +import { useParams } from 'react-router-dom'; + +import { StylishText } from '@lib/components/StylishText'; +import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; +import { ModelType } from '@lib/enums/ModelType'; +import { UserRoles } from '@lib/enums/Roles'; +import { apiUrl } from '@lib/functions/Api'; +import type { PanelType } from '@lib/types/Panel'; +import AdminButton from '../../components/buttons/AdminButton'; +import PrimaryActionButton from '../../components/buttons/PrimaryActionButton'; +import { PrintingActions } from '../../components/buttons/PrintingActions'; +import { + type DetailsField, + DetailsTable +} from '../../components/details/Details'; +import { DetailsImage } from '../../components/details/DetailsImage'; +import { ItemDetailsGrid } from '../../components/details/ItemDetails'; +import { + BarcodeActionDropdown, + CancelItemAction, + DuplicateItemAction, + EditItemAction, + HoldItemAction, + OptionsActionDropdown +} from '../../components/items/ActionDropdown'; +import InstanceDetail from '../../components/nav/InstanceDetail'; +import { PageDetail } from '../../components/nav/PageDetail'; +import AttachmentPanel from '../../components/panels/AttachmentPanel'; +import NotesPanel from '../../components/panels/NotesPanel'; +import { PanelGroup } from '../../components/panels/PanelGroup'; +import ParametersPanel from '../../components/panels/ParametersPanel'; +import { StatusRenderer } from '../../components/render/StatusRenderer'; +import { useRepairOrderFields } from '../../forms/RepairOrderForms'; +import { + useCreateApiFormModal, + useEditApiFormModal +} from '../../hooks/UseForm'; +import { useInstance } from '../../hooks/UseInstance'; +import useStatusCodes from '../../hooks/UseStatusCodes'; +import { useGlobalSettingsState } from '../../states/SettingsStates'; +import { useUserState } from '../../states/UserState'; +import RepairOrderLineItemTable from '../../tables/sales/RepairOrderLineItemTable'; + +export default function RepairOrderDetail() { + const { id } = useParams(); + const user = useUserState(); + const globalSettings = useGlobalSettingsState(); + + const { + instance: order, + instanceQuery, + refreshInstance + } = useInstance({ + endpoint: ApiEndpoints.repair_order_list, + pk: id, + params: { + customer_detail: true, + asset_detail: true + } + }); + + const roStatus = useStatusCodes({ modelType: ModelType.repairorder }); + + const orderOpen = useMemo(() => { + return ( + order.status == roStatus.PENDING || + order.status == roStatus.IN_PROGRESS || + order.status == roStatus.ON_HOLD + ); + }, [order, roStatus]); + + const lineItemsEditable: boolean = useMemo(() => { + return ( + orderOpen || globalSettings.isSet('RETURNORDER_EDIT_COMPLETED_ORDERS') + ); + }, [orderOpen, globalSettings]); + + const detailsPanel = useMemo(() => { + if (instanceQuery.isFetching) { + return ; + } + + const tl: DetailsField[] = [ + { + type: 'text', + name: 'reference', + label: t`Reference`, + copy: true + }, + { + type: 'link', + name: 'customer', + icon: 'customers', + label: t`Customer`, + model: ModelType.company + }, + { + type: 'link', + name: 'asset', + icon: 'part', + label: t`Fixed Asset`, + model: ModelType.stockitem + }, + { + type: 'text', + name: 'description', + label: t`Description`, + copy: true + }, + { + type: 'text', + name: 'symptoms', + label: t`Symptoms`, + copy: true, + hidden: !order.symptoms + }, + { + type: 'status', + name: 'status', + label: t`Status`, + model: ModelType.repairorder + }, + { + type: 'status', + name: 'status_custom_key', + label: t`Custom Status`, + model: ModelType.repairorder, + icon: 'status', + hidden: + !order.status_custom_key || order.status_custom_key == order.status + } + ]; + + const tr: DetailsField[] = [ + { + type: 'text', + name: 'line_items', + label: t`Line Items`, + icon: 'list' + } + ]; + + return ( + + + + + + + + + + ); + }, [order, instanceQuery]); + + const orderPanels: PanelType[] = useMemo(() => { + return [ + { + name: 'detail', + label: t`Order Details`, + icon: , + content: detailsPanel + }, + { + name: 'line-items', + label: t`Line Items`, + icon: , + content: ( + + + + {t`Line Items`} + + + + + + + ) + }, + ParametersPanel({ + model_type: ModelType.repairorder, + model_id: order.pk + }), + AttachmentPanel({ + model_type: ModelType.repairorder, + model_id: order.pk + }), + NotesPanel({ + model_type: ModelType.repairorder, + model_id: order.pk, + has_note: !!order.notes + }) + ]; + }, [order, id, user, lineItemsEditable]); + + const orderBadges: ReactNode[] = useMemo(() => { + return instanceQuery.isLoading + ? [] + : [ + + ]; + }, [order, instanceQuery]); + + const repairOrderFields = useRepairOrderFields({}); + + const duplicateRepairOrderFields = useRepairOrderFields({ + duplicateOrderId: order.pk + }); + + const editRepairOrder = useEditApiFormModal({ + url: ApiEndpoints.repair_order_list, + pk: order.pk, + title: t`Edit Repair Order`, + fields: repairOrderFields, + onFormSuccess: () => { + refreshInstance(); + } + }); + + const duplicateRepairOrderInitialData = useMemo(() => { + const data = { ...order }; + delete data.reference; + return data; + }, [order]); + + const duplicateRepairOrder = useCreateApiFormModal({ + url: ApiEndpoints.repair_order_list, + title: t`Add Repair Order`, + fields: duplicateRepairOrderFields, + initialData: duplicateRepairOrderInitialData, + modelType: ModelType.repairorder, + follow: true + }); + + const issueOrder = useCreateApiFormModal({ + url: apiUrl(ApiEndpoints.repair_order_issue, order.pk), + title: t`Issue Repair Order`, + onFormSuccess: refreshInstance, + preFormWarning: t`Issue this order`, + successMessage: t`Order issued` + }); + + const cancelOrder = useCreateApiFormModal({ + url: apiUrl(ApiEndpoints.repair_order_cancel, order.pk), + title: t`Cancel Repair Order`, + onFormSuccess: refreshInstance, + preFormWarning: t`Cancel this order`, + successMessage: t`Order cancelled` + }); + + const holdOrder = useCreateApiFormModal({ + url: apiUrl(ApiEndpoints.repair_order_hold, order.pk), + title: t`Hold Repair Order`, + onFormSuccess: refreshInstance, + preFormWarning: t`Place this order on hold`, + successMessage: t`Order placed on hold` + }); + + const completeOrder = useCreateApiFormModal({ + url: apiUrl(ApiEndpoints.repair_order_complete, order.pk), + title: t`Complete Repair Order`, + onFormSuccess: refreshInstance, + preFormWarning: t`Mark this order as complete`, + successMessage: t`Order completed` + }); + + const orderActions = useMemo(() => { + const canEdit: boolean = user.hasChangeRole(UserRoles.repair_order); + + const canIssue: boolean = + canEdit && + (order.status == roStatus.PENDING || order.status == roStatus.ON_HOLD); + + const canHold: boolean = + canEdit && + (order.status == roStatus.PENDING || + order.status == roStatus.IN_PROGRESS); + + const canCancel: boolean = + canEdit && + (order.status == roStatus.PENDING || + order.status == roStatus.IN_PROGRESS || + order.status == roStatus.ON_HOLD); + + const canComplete: boolean = + canEdit && order.status == roStatus.IN_PROGRESS; + + return [ +