From a00d5ab4b593ff68317a46d3eb359093c936969f Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 15 Mar 2024 17:53:58 +1100 Subject: [PATCH] Disable BOM requirement (#6719) * Add new setting STOCK_ENFORCE_BOM_INSTALLATION - Defaults to True (legacy) * Add logic to bypass BOM check * Update CUI to reflect new logic * Render InstalledItemsTable in PUI --- InvenTree/common/models.py | 8 ++ InvenTree/stock/serializers.py | 11 ++- InvenTree/stock/templates/stock/item.html | 3 + .../templates/InvenTree/settings/stock.html | 1 + InvenTree/templates/js/translated/stock.js | 2 +- src/frontend/src/enums/ApiEndpoints.tsx | 1 + .../pages/Index/Settings/SystemSettings.tsx | 3 +- src/frontend/src/pages/stock/StockDetail.tsx | 12 ++- .../src/tables/stock/InstalledItemsTable.tsx | 76 +++++++++++++++++++ 9 files changed, 111 insertions(+), 6 deletions(-) create mode 100644 src/frontend/src/tables/stock/InstalledItemsTable.tsx diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index b4a175a071..b3a1c12e6d 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -1750,6 +1750,14 @@ class InvenTreeSetting(BaseInvenTreeSetting): 'default': False, 'validator': bool, }, + 'STOCK_ENFORCE_BOM_INSTALLATION': { + 'name': _('Check BOM when installing items'), + 'description': _( + 'Installed stock items must exist in the BOM for the parent part' + ), + 'default': True, + 'validator': bool, + }, 'BUILDORDER_REFERENCE_PATTERN': { 'name': _('Build Order Reference Pattern'), 'description': _( diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 8d062f6855..8d1be0f805 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -584,9 +584,14 @@ class InstallStockItemSerializer(serializers.Serializer): parent_item = self.context['item'] parent_part = parent_item.part - # Check if the selected part is in the Bill of Materials of the parent item - if not parent_part.check_if_part_in_bom(stock_item.part): - raise ValidationError(_('Selected part is not in the Bill of Materials')) + if common.models.InvenTreeSetting.get_setting( + 'STOCK_ENFORCE_BOM_INSTALLATION', backup_value=True, cache=False + ): + # Check if the selected part is in the Bill of Materials of the parent item + if not parent_part.check_if_part_in_bom(stock_item.part): + raise ValidationError( + _('Selected part is not in the Bill of Materials') + ) return stock_item diff --git a/InvenTree/stock/templates/stock/item.html b/InvenTree/stock/templates/stock/item.html index 30272d53a9..0443e5eacc 100644 --- a/InvenTree/stock/templates/stock/item.html +++ b/InvenTree/stock/templates/stock/item.html @@ -183,7 +183,10 @@ $('#stock-item-install').click(function() { + {% settings_value "STOCK_ENFORCE_BOM_INSTALLATION" as enforce_bom %} + installStockItem({{ item.pk }}, {{ item.part.pk }}, { + enforce_bom: {% js_bool enforce_bom %}, onSuccess: function(response) { $("#installed-table").bootstrapTable('refresh'); } diff --git a/InvenTree/templates/InvenTree/settings/stock.html b/InvenTree/templates/InvenTree/settings/stock.html index 388118555c..39bdd51759 100644 --- a/InvenTree/templates/InvenTree/settings/stock.html +++ b/InvenTree/templates/InvenTree/settings/stock.html @@ -22,6 +22,7 @@ {% include "InvenTree/settings/setting.html" with key="STOCK_OWNERSHIP_CONTROL" icon="fa-users" %} {% include "InvenTree/settings/setting.html" with key="STOCK_LOCATION_DEFAULT_ICON" icon="fa-icons" %} {% include "InvenTree/settings/setting.html" with key="STOCK_SHOW_INSTALLED_ITEMS" icon="fa-sitemap" %} + {% include "InvenTree/settings/setting.html" with key="STOCK_ENFORCE_BOM_INSTALLATION" icon="fa-check-circle" %} diff --git a/InvenTree/templates/js/translated/stock.js b/InvenTree/templates/js/translated/stock.js index c6a1f5f2de..ddb65c8b5c 100644 --- a/InvenTree/templates/js/translated/stock.js +++ b/InvenTree/templates/js/translated/stock.js @@ -3204,7 +3204,7 @@ function installStockItem(stock_item_id, part_id, options={}) { auto_fill: true, filters: { trackable: true, - in_bom_for: part_id, + in_bom_for: options.enforce_bom ? part_id : undefined, } }, stock_item: { diff --git a/src/frontend/src/enums/ApiEndpoints.tsx b/src/frontend/src/enums/ApiEndpoints.tsx index 9bdb1b5ef2..5a35fa87cd 100644 --- a/src/frontend/src/enums/ApiEndpoints.tsx +++ b/src/frontend/src/enums/ApiEndpoints.tsx @@ -92,6 +92,7 @@ export enum ApiEndpoints { stock_merge = 'stock/merge/', stock_assign = 'stock/assign/', stock_status = 'stock/status/', + stock_install = 'stock/:id/install', // Order API endpoints purchase_order_list = 'order/po/', diff --git a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx index e449d08ecb..26ea9f8c79 100644 --- a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx +++ b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx @@ -212,7 +212,8 @@ export default function SystemSettings() { 'STOCK_ALLOW_EXPIRED_BUILD', 'STOCK_OWNERSHIP_CONTROL', 'STOCK_LOCATION_DEFAULT_ICON', - 'STOCK_SHOW_INSTALLED_ITEMS' + 'STOCK_SHOW_INSTALLED_ITEMS', + 'STOCK_ENFORCE_BOM_INSTALLATION' ]} /> ) diff --git a/src/frontend/src/pages/stock/StockDetail.tsx b/src/frontend/src/pages/stock/StockDetail.tsx index dd09d26483..19e75db4a8 100644 --- a/src/frontend/src/pages/stock/StockDetail.tsx +++ b/src/frontend/src/pages/stock/StockDetail.tsx @@ -55,6 +55,7 @@ import { useInstance } from '../../hooks/UseInstance'; import { apiUrl } from '../../states/ApiState'; import { useUserState } from '../../states/UserState'; import { AttachmentTable } from '../../tables/general/AttachmentTable'; +import InstalledItemsTable from '../../tables/stock/InstalledItemsTable'; import { StockItemTable } from '../../tables/stock/StockItemTable'; import StockItemTestResultTable from '../../tables/stock/StockItemTestResultTable'; @@ -164,6 +165,14 @@ export default function StockDetail() { type: 'link', name: 'belongs_to', label: t`Installed In`, + model_formatter: (model: any) => { + let text = model?.part_detail?.full_name ?? model?.name; + if (model.serial && model.quantity == 1) { + text += `# ${model.serial}`; + } + + return text; + }, model: ModelType.stockitem, hidden: !stockitem.belongs_to }, @@ -259,7 +268,8 @@ export default function StockDetail() { name: 'installed_items', label: t`Installed Items`, icon: , - hidden: !stockitem?.part_detail?.assembly + hidden: !stockitem?.part_detail?.assembly, + content: }, { name: 'child_items', diff --git a/src/frontend/src/tables/stock/InstalledItemsTable.tsx b/src/frontend/src/tables/stock/InstalledItemsTable.tsx new file mode 100644 index 0000000000..b503feaee2 --- /dev/null +++ b/src/frontend/src/tables/stock/InstalledItemsTable.tsx @@ -0,0 +1,76 @@ +import { useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { ApiEndpoints } from '../../enums/ApiEndpoints'; +import { ModelType } from '../../enums/ModelType'; +import { getDetailUrl } from '../../functions/urls'; +import { useTable } from '../../hooks/UseTable'; +import { apiUrl } from '../../states/ApiState'; +import { useUserState } from '../../states/UserState'; +import { TableColumn } from '../Column'; +import { PartColumn, StatusColumn } from '../ColumnRenderers'; +import { InvenTreeTable } from '../InvenTreeTable'; + +export default function InstalledItemsTable({ + parentId +}: { + parentId?: number | string; +}) { + const table = useTable('stock_item_install'); + const user = useUserState(); + const navigate = useNavigate(); + + const tableColumns: TableColumn[] = useMemo(() => { + return [ + { + accessor: 'part', + switchable: false, + render: (record: any) => PartColumn(record?.part_detail) + }, + { + accessor: 'quantity', + switchable: false, + render: (record: any) => { + let text = record.quantity; + + if (record.serial && record.quantity == 1) { + text = `# ${record.serial}`; + } + + return text; + } + }, + { + accessor: 'batch', + switchable: false + }, + StatusColumn(ModelType.stockitem) + ]; + }, []); + + const tableActions = useMemo(() => { + return []; + }, [user]); + + return ( + <> + { + if (record.pk) { + navigate(getDetailUrl(ModelType.stockitem, record.pk)); + } + }, + params: { + belongs_to: parentId, + part_detail: true + } + }} + /> + + ); +}