From 6650f3e90ce341253bf9233eea31634fead99a90 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 11 Jul 2024 14:33:53 +1000 Subject: [PATCH] [Feature] Build allocation export (#7611) * CUI: Add "allocated stock" panel to build order page * Implement CUI table for build order allocations * Add "bulk delete" option for build order allocations * Add row actions * Add extra fields for data export * Add build allocation table in PUI * Add 'batch' column * Bump API version * Add playwright tests * Fix missing renderer * Update build docs * Update playwright tests * Update playwright tests --- .../images/build/allocated_stock_table.png | Bin 0 -> 69387 bytes docs/docs/build/build.md | 21 +-- .../InvenTree/InvenTree/api_version.py | 6 +- src/backend/InvenTree/build/api.py | 30 ++- src/backend/InvenTree/build/serializers.py | 64 ++++--- .../build/templates/build/detail.html | 18 +- .../build/templates/build/sidebar.html | 10 +- .../templates/js/translated/build.js | 175 ++++++++++++++++++ src/frontend/src/components/render/Build.tsx | 6 + .../src/components/render/Instance.tsx | 3 +- .../src/components/render/ModelType.tsx | 5 + src/frontend/src/enums/ApiEndpoints.tsx | 1 + src/frontend/src/enums/ModelType.tsx | 1 + src/frontend/src/pages/build/BuildDetail.tsx | 20 +- .../tables/build/BuildAllocatedStockTable.tsx | 158 ++++++++++++++++ src/frontend/tests/pages/pui_build.spec.ts | 34 ++++ src/frontend/tests/pages/pui_part.spec.ts | 2 - src/frontend/tests/pui_stock.spec.ts | 14 -- 18 files changed, 500 insertions(+), 68 deletions(-) create mode 100644 docs/docs/assets/images/build/allocated_stock_table.png create mode 100644 src/frontend/src/tables/build/BuildAllocatedStockTable.tsx create mode 100644 src/frontend/tests/pages/pui_build.spec.ts diff --git a/docs/docs/assets/images/build/allocated_stock_table.png b/docs/docs/assets/images/build/allocated_stock_table.png new file mode 100644 index 0000000000000000000000000000000000000000..676f4492f9a910d87b23c58d403252f603b427bc GIT binary patch literal 69387 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 SgMQ^yNQk@^E_$iy^}hfcyeClr literal 0 HcmV?d00001 diff --git a/docs/docs/build/build.md b/docs/docs/build/build.md index 22f4084761..594b98846a 100644 --- a/docs/docs/build/build.md +++ b/docs/docs/build/build.md @@ -26,14 +26,6 @@ To navigate to the Build Order display, select *Build* from the main navigation {% include "img.html" %} {% endwith %} -#### Tree View - -*Tree View* also provides a tabulated view of Build Orders. Orders are displayed in a hierarchical manner, showing any parent / child relationships between different build orders. - -{% with id="build_tree", url="build/build_tree.png", description="Build Tree" %} -{% include "img.html" %} -{% endwith %} - #### Calendar View *Calendar View* shows a calendar display with upcoming build orders, based on the various dates specified for each build. @@ -121,9 +113,9 @@ The *Build Details* tab provides an overview of the Build Order: {% include "img.html" %} {% endwith %} -### Allocate Stock +### Line Items -The *Allocate Stock* tab provides an interface to allocate required stock (as specified by the BOM) to the build: +The *Line Items* tab provides an interface to allocate required stock (as specified by the BOM) to the build: {% with id="build_allocate", url="build/build_allocate.png", description="Allocation tab" %} {% include "img.html" %} @@ -131,8 +123,13 @@ The *Allocate Stock* tab provides an interface to allocate required stock (as sp The allocation table (as shown above) shows the stock allocation progress for this build. In the example above, there are two BOM lines, which have been partially allocated. -!!! info "Completed Builds" - The *Allocate Stock* tab is not available if the build has been completed! +### Allocated Stock + +The *Allocated Stock* tab displays all stock items which have been *allocated* to this build order. These stock items are reserved for this build, and will be consumed when the build is completed: + +{% with id="allocated_stock_table", url="build/allocated_stock_table.png", description="Allocated Stock Table" %} +{% include "img.html" %} +{% endwith %} ### Consumed Stock diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 19dc675787..bdb4330d90 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,12 +1,16 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 218 +INVENTREE_API_VERSION = 219 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v219 - 2024-07-11 : https://github.com/inventree/InvenTree/pull/7611 + - Adds new fields to the BuildItem API endpoints + - Adds new ordering / filtering options to the BuildItem API endpoints + v218 - 2024-07-11 : https://github.com/inventree/InvenTree/pull/7619 - Adds "can_build" field to the BomItem API diff --git a/src/backend/InvenTree/build/api.py b/src/backend/InvenTree/build/api.py index 8944ecb9a7..e21c60037d 100644 --- a/src/backend/InvenTree/build/api.py +++ b/src/backend/InvenTree/build/api.py @@ -8,12 +8,11 @@ from django.contrib.auth.models import User from rest_framework.exceptions import ValidationError -from django_filters.rest_framework import DjangoFilterBackend from django_filters import rest_framework as rest_filters from importer.mixins import DataExportViewMixin -from InvenTree.api import MetadataView +from InvenTree.api import BulkDeleteMixin, MetadataView from generic.states.api import StatusView from InvenTree.helpers import str2bool, isNull from build.status_codes import BuildStatus, BuildStatusGroups @@ -546,15 +545,17 @@ class BuildItemFilter(rest_filters.FilterSet): return queryset.filter(install_into=None) -class BuildItemList(DataExportViewMixin, ListCreateAPI): +class BuildItemList(DataExportViewMixin, BulkDeleteMixin, ListCreateAPI): """API endpoint for accessing a list of BuildItem objects. - GET: Return list of objects - POST: Create a new BuildItem object """ + queryset = BuildItem.objects.all() serializer_class = build.serializers.BuildItemSerializer filterset_class = BuildItemFilter + filter_backends = SEARCH_ORDER_FILTER_ALIAS def get_serializer(self, *args, **kwargs): """Returns a BuildItemSerializer instance based on the request.""" @@ -571,7 +572,7 @@ class BuildItemList(DataExportViewMixin, ListCreateAPI): def get_queryset(self): """Override the queryset method, to allow filtering by stock_item.part.""" - queryset = BuildItem.objects.all() + queryset = super().get_queryset() queryset = queryset.select_related( 'build_line', @@ -607,8 +608,25 @@ class BuildItemList(DataExportViewMixin, ListCreateAPI): return queryset - filter_backends = [ - DjangoFilterBackend, + ordering_fields = [ + 'part', + 'sku', + 'quantity', + 'location', + 'reference', + ] + + ordering_field_aliases = { + 'part': 'stock_item__part__name', + 'sku': 'stock_item__supplier_part__SKU', + 'location': 'stock_item__location__name', + 'reference': 'build_line__bom_item__reference', + } + + search_fields = [ + 'stock_item__supplier_part__SKU', + 'stock_item__part__name', + 'build_line__bom_item__reference', ] diff --git a/src/backend/InvenTree/build/serializers.py b/src/backend/InvenTree/build/serializers.py index 83b046c0a7..ef84817123 100644 --- a/src/backend/InvenTree/build/serializers.py +++ b/src/backend/InvenTree/build/serializers.py @@ -26,8 +26,9 @@ from stock.serializers import StockItemSerializerBrief, LocationSerializer import common.models from common.serializers import ProjectCodeSerializer from importer.mixins import DataImportExportSerializerMixin +import company.serializers import part.filters -from part.serializers import BomItemSerializer, PartSerializer, PartBriefSerializer +import part.serializers as part_serializers from users.serializers import OwnerSerializer from .models import Build, BuildLine, BuildItem @@ -85,7 +86,7 @@ class BuildSerializer(NotesFieldMixin, DataImportExportSerializerMixin, InvenTre status_text = serializers.CharField(source='get_status_display', read_only=True) - part_detail = PartBriefSerializer(source='part', many=False, read_only=True) + part_detail = part_serializers.PartBriefSerializer(source='part', many=False, read_only=True) part_name = serializers.CharField(source='part.name', read_only=True, label=_('Part Name')) @@ -1062,10 +1063,13 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali # These fields are only used for data export export_only_fields = [ 'build_reference', - 'bom_reference', 'sku', 'mpn', 'location_name', + 'part_id', + 'part_name', + 'part_ipn', + 'available_quantity', ] class Meta: @@ -1085,6 +1089,7 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali 'location_detail', 'part_detail', 'stock_item_detail', + 'supplier_part_detail', # The following fields are only used for data export 'bom_reference', @@ -1092,27 +1097,12 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali 'location_name', 'mpn', 'sku', + 'part_id', + 'part_name', + 'part_ipn', + 'available_quantity', ] - # Export-only fields - sku = serializers.CharField(source='stock_item.supplier_part.SKU', label=_('Supplier Part Number'), read_only=True) - mpn = serializers.CharField(source='stock_item.supplier_part.manufacturer_part.MPN', label=_('Manufacturer Part Number'), read_only=True) - location_name = serializers.CharField(source='stock_item.location.name', label=_('Location Name'), read_only=True) - build_reference = serializers.CharField(source='build.reference', label=_('Build Reference'), read_only=True) - bom_reference = serializers.CharField(source='build_line.bom_item.reference', label=_('BOM Reference'), read_only=True) - - # Annotated fields - build = serializers.PrimaryKeyRelatedField(source='build_line.build', many=False, read_only=True) - - # Extra (optional) detail fields - part_detail = PartBriefSerializer(source='stock_item.part', many=False, read_only=True, pricing=False) - stock_item_detail = StockItemSerializerBrief(source='stock_item', read_only=True) - location = serializers.PrimaryKeyRelatedField(source='stock_item.location', many=False, read_only=True) - location_detail = LocationSerializer(source='stock_item.location', read_only=True) - build_detail = BuildSerializer(source='build_line.build', many=False, read_only=True) - - quantity = InvenTreeDecimalField() - def __init__(self, *args, **kwargs): """Determine which extra details fields should be included""" part_detail = kwargs.pop('part_detail', True) @@ -1134,6 +1124,32 @@ class BuildItemSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali if not build_detail: self.fields.pop('build_detail', None) + # Export-only fields + sku = serializers.CharField(source='stock_item.supplier_part.SKU', label=_('Supplier Part Number'), read_only=True) + mpn = serializers.CharField(source='stock_item.supplier_part.manufacturer_part.MPN', label=_('Manufacturer Part Number'), read_only=True) + location_name = serializers.CharField(source='stock_item.location.name', label=_('Location Name'), read_only=True) + build_reference = serializers.CharField(source='build.reference', label=_('Build Reference'), read_only=True) + bom_reference = serializers.CharField(source='build_line.bom_item.reference', label=_('BOM Reference'), read_only=True) + + # Part detail fields + part_id = serializers.PrimaryKeyRelatedField(source='stock_item.part', label=_('Part ID'), many=False, read_only=True) + part_name = serializers.CharField(source='stock_item.part.name', label=_('Part Name'), read_only=True) + part_ipn = serializers.CharField(source='stock_item.part.IPN', label=_('Part IPN'), read_only=True) + + # Annotated fields + build = serializers.PrimaryKeyRelatedField(source='build_line.build', many=False, read_only=True) + + # Extra (optional) detail fields + part_detail = part_serializers.PartBriefSerializer(source='stock_item.part', many=False, read_only=True, pricing=False) + stock_item_detail = StockItemSerializerBrief(source='stock_item', read_only=True) + location = serializers.PrimaryKeyRelatedField(source='stock_item.location', many=False, read_only=True) + location_detail = LocationSerializer(source='stock_item.location', read_only=True) + build_detail = BuildSerializer(source='build_line.build', many=False, read_only=True) + supplier_part_detail = company.serializers.SupplierPartSerializer(source='stock_item.supplier_part', many=False, read_only=True) + + quantity = InvenTreeDecimalField(label=_('Allocated Quantity')) + available_quantity = InvenTreeDecimalField(source='stock_item.quantity', read_only=True, label=_('Available Quantity')) + class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSerializer): """Serializer for a BuildItem object.""" @@ -1217,8 +1233,8 @@ class BuildLineSerializer(DataImportExportSerializerMixin, InvenTreeModelSeriali bom_item = serializers.PrimaryKeyRelatedField(label=_('BOM Item'), read_only=True) # Foreign key fields - bom_item_detail = BomItemSerializer(source='bom_item', many=False, read_only=True, pricing=False) - part_detail = PartSerializer(source='bom_item.sub_part', many=False, read_only=True, pricing=False) + bom_item_detail = part_serializers.BomItemSerializer(source='bom_item', many=False, read_only=True, pricing=False) + part_detail = part_serializers.PartSerializer(source='bom_item.sub_part', many=False, read_only=True, pricing=False) allocations = BuildItemSerializer(many=True, read_only=True) # Annotated (calculated) fields diff --git a/src/backend/InvenTree/build/templates/build/detail.html b/src/backend/InvenTree/build/templates/build/detail.html index 7daa1dc218..6cabe1f01c 100644 --- a/src/backend/InvenTree/build/templates/build/detail.html +++ b/src/backend/InvenTree/build/templates/build/detail.html @@ -174,7 +174,7 @@
-

{% trans "Allocate Stock to Build" %}

+

{% trans "Build Order Line Items" %}

{% include "spacer.html" %}
{% if roles.build.add and build.active %} @@ -231,6 +231,18 @@
+
+
+

{% trans "Allocated Stock" %}

+
+
+
+ {% include "filter_list.html" with id='buildorderallocatedstock' %} +
+
+
+
+

@@ -290,6 +302,10 @@ {% block js_ready %} {{ block.super }} +onPanelLoad('allocated', function() { + loadBuildOrderAllocatedStockTable($('#allocated-stock-table'), {{ build.pk }}); +}); + onPanelLoad('consumed', function() { loadStockTable($('#consumed-stock-table'), { filterTarget: '#filter-list-consumed-stock', diff --git a/src/backend/InvenTree/build/templates/build/sidebar.html b/src/backend/InvenTree/build/templates/build/sidebar.html index c038b7782a..1c20429f2f 100644 --- a/src/backend/InvenTree/build/templates/build/sidebar.html +++ b/src/backend/InvenTree/build/templates/build/sidebar.html @@ -5,15 +5,19 @@ {% trans "Build Order Details" as text %} {% include "sidebar_item.html" with label='details' text=text icon="fa-info-circle" %} {% if build.is_active %} -{% trans "Allocate Stock" as text %} -{% include "sidebar_item.html" with label='allocate' text=text icon="fa-tasks" %} +{% trans "Line Items" as text %} +{% include "sidebar_item.html" with label='allocate' text=text icon="fa-list-ol" %} {% trans "Incomplete Outputs" as text %} {% include "sidebar_item.html" with label='outputs' text=text icon="fa-tools" %} {% endif %} {% trans "Completed Outputs" as text %} {% include "sidebar_item.html" with label='completed' text=text icon="fa-boxes" %} +{% if build.is_active %} +{% trans "Allocated Stock" as text %} +{% include "sidebar_item.html" with label='allocated' text=text icon="fa-list" %} +{% endif %} {% trans "Consumed Stock" as text %} -{% include "sidebar_item.html" with label='consumed' text=text icon="fa-list" %} +{% include "sidebar_item.html" with label='consumed' text=text icon="fa-tasks" %} {% trans "Child Build Orders" as text %} {% include "sidebar_item.html" with label='children' text=text icon="fa-sitemap" %} {% trans "Attachments" as text %} diff --git a/src/backend/InvenTree/templates/js/translated/build.js b/src/backend/InvenTree/templates/js/translated/build.js index 5a762d2bd5..d6e5daf95a 100644 --- a/src/backend/InvenTree/templates/js/translated/build.js +++ b/src/backend/InvenTree/templates/js/translated/build.js @@ -58,6 +58,7 @@ duplicateBuildOrder, editBuildOrder, loadBuildLineTable, + loadBuildOrderAllocatedStockTable, loadBuildOrderAllocationTable, loadBuildOutputTable, loadBuildTable, @@ -933,6 +934,180 @@ function deleteBuildOutputs(build_id, outputs, options={}) { } +/** + * Load a table showing all stock allocated to a given Build Order + */ +function loadBuildOrderAllocatedStockTable(table, buildId) { + + let params = { + build: buildId, + part_detail: true, + location_detail: true, + stock_detail: true, + supplier_detail: true, + }; + + let filters = loadTableFilters('buildorderallocatedstock', params); + setupFilterList( + 'buildorderallocatedstock', + $(table), + null, + { + download: true, + custom_actions: [{ + label: 'actions', + actions: [{ + label: 'delete', + title: '{% trans "Delete allocations" %}', + icon: 'fa-trash-alt icon-red', + permission: 'build.delete', + callback: function(data) { + constructForm('{% url "api-build-item-list" %}', { + method: 'DELETE', + multi_delete: true, + title: '{% trans "Delete Stock Allocations" %}', + form_data: { + items: data.map(item => item.pk), + }, + onSuccess: function() { + $(table).bootstrapTable('refresh'); + } + }); + } + }] + }] + } + ); + + $(table).inventreeTable({ + url: '{% url "api-build-item-list" %}', + queryParams: filters, + original: params, + sortable: true, + search: true, + groupBy: false, + sidePagination: 'server', + formatNoMatches: function() { + return '{% trans "No allocated stock" %}'; + }, + columns: [ + { + title: '', + visible: true, + checkbox: true, + switchable: false, + }, + { + field: 'part', + sortable: true, + switchable: false, + title: '{% trans "Part" %}', + formatter: function(value, row) { + return imageHoverIcon(row.part_detail.thumbnail) + renderLink(row.part_detail.full_name, `/part/${row.part_detail.pk}/`); + } + }, + { + field: 'bom_reference', + sortable: true, + switchable: true, + title: '{% trans "Reference" %}', + }, + { + field: 'quantity', + sortable: true, + switchable: false, + title: '{% trans "Allocated Quantity" %}', + formatter: function(value, row) { + let stock_item = row.stock_item_detail; + let text = value; + + if (stock_item.serial && stock_item.quantity == 1) { + text = `# ${stock_item.serial}`; + } + + return renderLink(text, `/stock/item/${stock_item.pk}/`); + } + }, + { + field: 'location', + sortable: true, + title: '{% trans "Location" %}', + formatter: function(value, row) { + if (row.location_detail) { + return locationDetail(row, true); + } + } + }, + { + field: 'install_into', + sortable: true, + title: '{% trans "Build Output" %}', + formatter: function(value, row) { + if (value) { + return renderLink(`{% trans "Stock item" %}: ${value}`, `/stock/item/${value}/`); + } + } + }, + { + field: 'sku', + sortable: true, + title: '{% trans "Supplier Part" %}', + formatter: function(value, row) { + if (row.supplier_part_detail) { + let text = row.supplier_part_detail.SKU; + + return renderLink(text, `/supplier-part/${row.supplier_part_detail.pk}/`); + } + } + }, + { + field: 'pk', + title: '{% trans "Actions" %}', + visible: true, + switchable: false, + sortable: false, + formatter: function(value, row) { + let buttons = ''; + + buttons += makeEditButton('build-item-edit', row.pk, '{% trans "Edit build allocation" %}'); + buttons += makeDeleteButton('build-item-delete', row.pk, '{% trans "Delete build allocation" %}'); + + return wrapButtons(buttons); + } + } + ] + }); + + // Add row callbacks + $(table).on('click', '.build-item-edit', function() { + let pk = $(this).attr('pk'); + + constructForm( + `/api/build/item/${pk}/`, + { + fields: { + quantity: {}, + }, + title: '{% trans "Edit Build Allocation" %}', + refreshTable: table + } + ); + }); + + $(table).on('click', '.build-item-delete', function() { + let pk = $(this).attr('pk'); + + constructForm( + `/api/build/item/${pk}/`, + { + method: 'DELETE', + title: '{% trans "Delete Build Allocation" %}', + refreshTable: table, + } + ); + }); +} + /** * Load a table showing all the BuildOrder allocations for a given part */ diff --git a/src/frontend/src/components/render/Build.tsx b/src/frontend/src/components/render/Build.tsx index 37f8eacde0..90ddbf6cc3 100644 --- a/src/frontend/src/components/render/Build.tsx +++ b/src/frontend/src/components/render/Build.tsx @@ -46,3 +46,9 @@ export function RenderBuildLine({ /> ); } + +export function RenderBuildItem({ + instance +}: Readonly): ReactNode { + return ; +} diff --git a/src/frontend/src/components/render/Instance.tsx b/src/frontend/src/components/render/Instance.tsx index cc179631c2..58a7261834 100644 --- a/src/frontend/src/components/render/Instance.tsx +++ b/src/frontend/src/components/render/Instance.tsx @@ -8,7 +8,7 @@ import { ModelType } from '../../enums/ModelType'; import { navigateToLink } from '../../functions/navigation'; import { apiUrl } from '../../states/ApiState'; import { Thumbnail } from '../images/Thumbnail'; -import { RenderBuildLine, RenderBuildOrder } from './Build'; +import { RenderBuildItem, RenderBuildLine, RenderBuildOrder } from './Build'; import { RenderAddress, RenderCompany, @@ -59,6 +59,7 @@ const RendererLookup: EnumDictionary< [ModelType.address]: RenderAddress, [ModelType.build]: RenderBuildOrder, [ModelType.buildline]: RenderBuildLine, + [ModelType.builditem]: RenderBuildItem, [ModelType.company]: RenderCompany, [ModelType.contact]: RenderContact, [ModelType.manufacturerpart]: RenderManufacturerPart, diff --git a/src/frontend/src/components/render/ModelType.tsx b/src/frontend/src/components/render/ModelType.tsx index d96cc28bb0..e8b12da9ca 100644 --- a/src/frontend/src/components/render/ModelType.tsx +++ b/src/frontend/src/components/render/ModelType.tsx @@ -113,6 +113,11 @@ export const ModelInformationDict: ModelDict = { cui_detail: '/build/line/:pk/', api_endpoint: ApiEndpoints.build_line_list }, + builditem: { + label: t`Build Item`, + label_multiple: t`Build Items`, + api_endpoint: ApiEndpoints.build_item_list + }, company: { label: t`Company`, label_multiple: t`Companies`, diff --git a/src/frontend/src/enums/ApiEndpoints.tsx b/src/frontend/src/enums/ApiEndpoints.tsx index a193216464..5e8aeda199 100644 --- a/src/frontend/src/enums/ApiEndpoints.tsx +++ b/src/frontend/src/enums/ApiEndpoints.tsx @@ -65,6 +65,7 @@ export enum ApiEndpoints { build_output_scrap = 'build/:id/scrap-outputs/', build_output_delete = 'build/:id/delete-outputs/', build_line_list = 'build/line/', + build_item_list = 'build/item/', bom_list = 'bom/', bom_item_validate = 'bom/:id/validate/', diff --git a/src/frontend/src/enums/ModelType.tsx b/src/frontend/src/enums/ModelType.tsx index 90492b98f0..e71944f954 100644 --- a/src/frontend/src/enums/ModelType.tsx +++ b/src/frontend/src/enums/ModelType.tsx @@ -15,6 +15,7 @@ export enum ModelType { stockhistory = 'stockhistory', build = 'build', buildline = 'buildline', + builditem = 'builditem', company = 'company', purchaseorder = 'purchaseorder', purchaseorderline = 'purchaseorderline', diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx index 28e9929b9c..c5635d7278 100644 --- a/src/frontend/src/pages/build/BuildDetail.tsx +++ b/src/frontend/src/pages/build/BuildDetail.tsx @@ -7,6 +7,7 @@ import { IconInfoCircle, IconList, IconListCheck, + IconListNumbers, IconNotes, IconPaperclip, IconQrcode, @@ -45,6 +46,7 @@ import { import { useInstance } from '../../hooks/UseInstance'; import { apiUrl } from '../../states/ApiState'; import { useUserState } from '../../states/UserState'; +import BuildAllocatedStockTable from '../../tables/build/BuildAllocatedStockTable'; import BuildLineTable from '../../tables/build/BuildLineTable'; import { BuildOrderTable } from '../../tables/build/BuildOrderTable'; import BuildOutputTable from '../../tables/build/BuildOutputTable'; @@ -233,9 +235,9 @@ export default function BuildDetail() { content: detailsPanel }, { - name: 'allocate-stock', - label: t`Allocate Stock`, - icon: , + name: 'line-items', + label: t`Line Items`, + icon: , content: build?.pk ? ( ) }, + { + name: 'allocated-stock', + label: t`Allocated Stock`, + icon: , + content: build.pk ? ( + + ) : ( + + ) + }, { name: 'consumed-stock', label: t`Consumed Stock`, - icon: , + icon: , content: ( { + return [ + { + name: 'tracked', + label: t`Allocated to Output`, + description: t`Show items allocated to a build output` + } + ]; + }, []); + + const tableColumns: TableColumn[] = useMemo(() => { + return [ + { + accessor: 'part', + title: t`Part`, + sortable: true, + switchable: false, + render: (record: any) => PartColumn(record.part_detail) + }, + { + accessor: 'bom_reference', + title: t`Reference`, + sortable: true, + switchable: true + }, + { + accessor: 'quantity', + title: t`Allocated Quantity`, + sortable: true, + switchable: false + }, + { + accessor: 'batch', + title: t`Batch Code`, + sortable: false, + switchable: true, + render: (record: any) => record?.stock_item_detail?.batch + }, + { + accessor: 'available', + title: t`Available Quantity`, + render: (record: any) => record?.stock_item_detail?.quantity + }, + LocationColumn({ + accessor: 'location_detail', + switchable: true, + sortable: true + }), + { + accessor: 'install_into', + title: t`Build Output`, + sortable: true + }, + { + accessor: 'sku', + title: t`Supplier Part`, + render: (record: any) => record?.supplier_part_detail?.SKU, + sortable: true + } + ]; + }, []); + + const [selectedItem, setSelectedItem] = useState(0); + + const editItem = useEditApiFormModal({ + pk: selectedItem, + url: ApiEndpoints.build_item_list, + title: t`Edit Build Item`, + fields: { + quantity: {} + }, + table: table + }); + + const deleteItem = useDeleteApiFormModal({ + pk: selectedItem, + url: ApiEndpoints.build_item_list, + title: t`Delete Build Item`, + table: table + }); + + const rowActions = useCallback( + (record: any) => { + return [ + RowEditAction({ + hidden: !user.hasChangeRole(UserRoles.build), + onClick: () => { + setSelectedItem(record.pk); + editItem.open(); + } + }), + RowDeleteAction({ + hidden: !user.hasDeleteRole(UserRoles.build), + onClick: () => { + setSelectedItem(record.pk); + deleteItem.open(); + } + }) + ]; + }, + [user] + ); + + return ( + <> + {editItem.modal} + {deleteItem.modal} + + + ); +} diff --git a/src/frontend/tests/pages/pui_build.spec.ts b/src/frontend/tests/pages/pui_build.spec.ts new file mode 100644 index 0000000000..0703380ee1 --- /dev/null +++ b/src/frontend/tests/pages/pui_build.spec.ts @@ -0,0 +1,34 @@ +import { test } from '../baseFixtures.ts'; +import { baseUrl } from '../defaults.ts'; +import { doQuickLogin } from '../login.ts'; + +test('PUI - Pages - Build Order', async ({ page }) => { + await doQuickLogin(page); + + await page.goto(`${baseUrl}/part/`); + + // Navigate to the correct build order + await page.getByRole('tab', { name: 'Build', exact: true }).click(); + await page.getByRole('cell', { name: 'BO0011' }).click(); + + // Click on some tabs + await page.getByRole('tab', { name: 'Attachments' }).click(); + await page.getByRole('tab', { name: 'Notes' }).click(); + await page.getByRole('tab', { name: 'Incomplete Outputs' }).click(); + await page.getByRole('tab', { name: 'Line Items' }).click(); + await page.getByRole('tab', { name: 'Allocated Stock' }).click(); + + // Check for expected text in the table + await page.getByText('R_10R_0402_1%').click(); + await page + .getByRole('cell', { name: 'R38, R39, R40, R41, R42, R43' }) + .click(); + + // Click through to the "parent" build + await page.getByRole('tab', { name: 'Build Details' }).click(); + await page.getByRole('link', { name: 'BO0010' }).click(); + await page + .getByLabel('Build Details') + .getByText('Making a high level assembly') + .waitFor(); +}); diff --git a/src/frontend/tests/pages/pui_part.spec.ts b/src/frontend/tests/pages/pui_part.spec.ts index 2e9b6938e6..af999de84b 100644 --- a/src/frontend/tests/pages/pui_part.spec.ts +++ b/src/frontend/tests/pages/pui_part.spec.ts @@ -116,8 +116,6 @@ test('PUI - Pages - Part - Pricing (Variant)', async ({ page }) => { // Variant Pricing await page.getByRole('button', { name: 'Variant Pricing' }).click(); - await page.waitForTimeout(500); - await page.getByRole('button', { name: 'Variant Part Not sorted' }).click(); // Variant Pricing - linkjumping let target = page.getByText('Green Chair').first(); diff --git a/src/frontend/tests/pui_stock.spec.ts b/src/frontend/tests/pui_stock.spec.ts index bcd08cdcde..46535302a9 100644 --- a/src/frontend/tests/pui_stock.spec.ts +++ b/src/frontend/tests/pui_stock.spec.ts @@ -32,20 +32,6 @@ test('PUI - Stock', async ({ page }) => { await page.getByRole('tab', { name: 'Installed Items' }).click(); }); -test('PUI - Build', async ({ page }) => { - await doQuickLogin(page); - - await page.getByRole('tab', { name: 'Build' }).click(); - await page.getByText('Widget Assembly Variant').click(); - await page.getByRole('tab', { name: 'Allocate Stock' }).click(); - await page.getByRole('tab', { name: 'Incomplete Outputs' }).click(); - await page.getByRole('tab', { name: 'Completed Outputs' }).click(); - await page.getByRole('tab', { name: 'Consumed Stock' }).click(); - await page.getByRole('tab', { name: 'Child Build Orders' }).click(); - await page.getByRole('tab', { name: 'Attachments' }).click(); - await page.getByRole('tab', { name: 'Notes' }).click(); -}); - test('PUI - Purchasing', async ({ page }) => { await doQuickLogin(page);