diff --git a/src/frontend/src/components/items/ActionDropdown.tsx b/src/frontend/src/components/items/ActionDropdown.tsx index 2e8a2df425..48603c880c 100644 --- a/src/frontend/src/components/items/ActionDropdown.tsx +++ b/src/frontend/src/components/items/ActionDropdown.tsx @@ -20,7 +20,7 @@ import { ReactNode, useMemo } from 'react'; import { ModelType } from '../../enums/ModelType'; import { identifierString } from '../../functions/conversion'; import { InvenTreeIcon } from '../../functions/icons'; -import { InvenTreeQRCode } from './QRCode'; +import { InvenTreeQRCode, QRCodeLink, QRCodeUnlink } from './QRCode'; export type ActionDropdownItem = { icon?: ReactNode; @@ -112,69 +112,91 @@ export function ActionDropdown({ // Dropdown menu for barcode actions export function BarcodeActionDropdown({ - actions -}: { - actions: ActionDropdownItem[]; -}) { + model, + pk, + hash = null, + actions = [], + perm: permission = true +}: Readonly<{ + model: ModelType; + pk: number; + hash?: boolean | null; + actions?: ActionDropdownItem[]; + perm?: boolean; +}>) { + const hidden = hash === null; + const prop = { model, pk, hash }; return ( } - actions={actions} + actions={[ + GeneralBarcodeAction({ + mdl_prop: prop, + title: t`View`, + icon: , + tooltip: t`View barcode`, + ChildItem: InvenTreeQRCode + }), + GeneralBarcodeAction({ + hidden: hidden || hash || !permission, + mdl_prop: prop, + title: t`Link Barcode`, + icon: , + tooltip: t`Link a custom barcode to this item`, + ChildItem: QRCodeLink + }), + GeneralBarcodeAction({ + hidden: hidden || !hash || !permission, + mdl_prop: prop, + title: t`Unlink Barcode`, + icon: , + tooltip: t`Unlink custom barcode`, + ChildItem: QRCodeUnlink + }), + ...actions + ]} /> ); } -// Common action button for viewing a barcode -export function ViewBarcodeAction({ - hidden = false, - model, - pk -}: { - hidden?: boolean; +export type QrCodeType = { model: ModelType; pk: number; + hash?: boolean | null; +}; + +function GeneralBarcodeAction({ + hidden = false, + mdl_prop, + title, + icon, + tooltip, + ChildItem +}: { + hidden?: boolean; + mdl_prop: QrCodeType; + title: string; + icon: ReactNode; + tooltip: string; + ChildItem: any; }): ActionDropdownItem { const onClick = () => { modals.open({ - title: t`View Barcode`, - children: + title: title, + children: }); }; return { - icon: , - name: t`View`, - tooltip: t`View barcode`, + icon: icon, + name: title, + tooltip: tooltip, onClick: onClick, hidden: hidden }; } -// Common action button for linking a custom barcode -export function LinkBarcodeAction( - props: ActionDropdownItem -): ActionDropdownItem { - return { - ...props, - icon: , - name: t`Link Barcode`, - tooltip: t`Link custom barcode` - }; -} - -// Common action button for un-linking a custom barcode -export function UnlinkBarcodeAction( - props: ActionDropdownItem -): ActionDropdownItem { - return { - ...props, - icon: , - name: t`Unlink Barcode`, - tooltip: t`Unlink custom barcode` - }; -} - // Common action button for editing an item export function EditItemAction(props: ActionDropdownItem): ActionDropdownItem { return { diff --git a/src/frontend/src/components/items/QRCode.tsx b/src/frontend/src/components/items/QRCode.tsx index 1077692314..8038ff8c23 100644 --- a/src/frontend/src/components/items/QRCode.tsx +++ b/src/frontend/src/components/items/QRCode.tsx @@ -1,24 +1,28 @@ import { Trans, t } from '@lingui/macro'; import { + Alert, Box, + Button, Code, Group, Image, Select, Skeleton, Stack, - Text + Text, + TextInput } from '@mantine/core'; +import { modals } from '@mantine/modals'; import { useQuery } from '@tanstack/react-query'; import QR from 'qrcode'; import { useEffect, useMemo, useState } from 'react'; import { api } from '../../App'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; -import { ModelType } from '../../enums/ModelType'; import { apiUrl } from '../../states/ApiState'; import { useGlobalSettingsState } from '../../states/SettingsState'; import { CopyButton } from '../buttons/CopyButton'; +import { QrCodeType } from './ActionDropdown'; type QRCodeProps = { ecl?: 'L' | 'M' | 'Q' | 'H'; @@ -51,15 +55,13 @@ export const QRCode = ({ data, ecl = 'Q', margin = 1 }: QRCodeProps) => { }; type InvenTreeQRCodeProps = { - model: ModelType; - pk: number; + mdl_prop: QrCodeType; showEclSelector?: boolean; } & Omit; export const InvenTreeQRCode = ({ + mdl_prop, showEclSelector = true, - model, - pk, ecl: eclProp = 'Q', ...props }: InvenTreeQRCodeProps) => { @@ -71,11 +73,11 @@ export const InvenTreeQRCode = ({ }, [eclProp]); const { data } = useQuery({ - queryKey: ['qr-code', model, pk], + queryKey: ['qr-code', mdl_prop.model, mdl_prop.pk], queryFn: async () => { const res = await api.post(apiUrl(ApiEndpoints.generate_barcode), { - model, - pk + model: mdl_prop.model, + pk: mdl_prop.pk }); return res.data?.barcode as string; @@ -94,6 +96,15 @@ export const InvenTreeQRCode = ({ return ( + {mdl_prop.hash ? ( + + + A custom barcode is registered for this item. The shown code is not + that custom barcode. + + + ) : null} + {data && settings.getSetting('BARCODE_SHOW_TEXT', 'false') && ( @@ -128,3 +139,55 @@ export const InvenTreeQRCode = ({ ); }; + +export const QRCodeLink = ({ mdl_prop }: { mdl_prop: QrCodeType }) => { + const [barcode, setBarcode] = useState(''); + + function linkBarcode() { + api + .post(apiUrl(ApiEndpoints.barcode_link), { + [mdl_prop.model]: mdl_prop.pk, + barcode: barcode + }) + .then((response) => { + modals.closeAll(); + location.reload(); + }); + } + return ( + + setBarcode(event.currentTarget.value)} + placeholder={t`Scan barcode data here using barcode scanner`} + /> + + + ); +}; + +export const QRCodeUnlink = ({ mdl_prop }: { mdl_prop: QrCodeType }) => { + function unlinkBarcode() { + api + .post(apiUrl(ApiEndpoints.barcode_unlink), { + [mdl_prop.model]: mdl_prop.pk + }) + .then((response) => { + modals.closeAll(); + location.reload(); + }); + } + return ( + + + This will remove the link to the associated barcode + + + + ); +}; diff --git a/src/frontend/src/enums/ApiEndpoints.tsx b/src/frontend/src/enums/ApiEndpoints.tsx index 740b73336e..1908a59577 100644 --- a/src/frontend/src/enums/ApiEndpoints.tsx +++ b/src/frontend/src/enums/ApiEndpoints.tsx @@ -39,6 +39,8 @@ export enum ApiEndpoints { settings_global_list = 'settings/global/', settings_user_list = 'settings/user/', barcode = 'barcode/', + barcode_link = 'barcode/link/', + barcode_unlink = 'barcode/unlink/', generate_barcode = 'barcode/generate/', news = 'news/', global_status = 'generic/status/', diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx index 1348ff1e00..bc9a02981f 100644 --- a/src/frontend/src/pages/build/BuildDetail.tsx +++ b/src/frontend/src/pages/build/BuildDetail.tsx @@ -30,10 +30,7 @@ import { CancelItemAction, DuplicateItemAction, EditItemAction, - HoldItemAction, - LinkBarcodeAction, - UnlinkBarcodeAction, - ViewBarcodeAction + HoldItemAction } from '../../components/items/ActionDropdown'; import InstanceDetail from '../../components/nav/InstanceDetail'; import { PageDetail } from '../../components/nav/PageDetail'; @@ -43,7 +40,6 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; import { UserRoles } from '../../enums/Roles'; import { useBuildOrderFields } from '../../forms/BuildForms'; -import { notYetImplemented } from '../../functions/notifications'; import { useCreateApiFormModal, useEditApiFormModal @@ -472,20 +468,9 @@ export default function BuildDetail() { />, , , , , , , , , , , , , , , , , location.pk ? ( , diff --git a/src/frontend/src/pages/stock/StockDetail.tsx b/src/frontend/src/pages/stock/StockDetail.tsx index 5a3f0d00e4..e787d210a4 100644 --- a/src/frontend/src/pages/stock/StockDetail.tsx +++ b/src/frontend/src/pages/stock/StockDetail.tsx @@ -27,10 +27,7 @@ import { BarcodeActionDropdown, DeleteItemAction, DuplicateItemAction, - EditItemAction, - LinkBarcodeAction, - UnlinkBarcodeAction, - ViewBarcodeAction + EditItemAction } from '../../components/items/ActionDropdown'; import { StylishText } from '../../components/items/StylishText'; import InstanceDetail from '../../components/nav/InstanceDetail'; @@ -50,7 +47,6 @@ import { useTransferStockItem } from '../../forms/StockForms'; import { InvenTreeIcon } from '../../functions/icons'; -import { notYetImplemented } from '../../functions/notifications'; import { getDetailUrl } from '../../functions/urls'; import { useCreateApiFormModal, @@ -477,22 +473,10 @@ export default function StockDetail() { () => [ , , { await page.getByRole('cell', { name: 'PO0013' }).click(); await page.getByRole('button', { name: 'Issue Order' }).waitFor(); +}); + +test('PUI - Purchase Orders - Barcodes', async ({ page }) => { + await doQuickLogin(page); + + await page.goto(`${baseUrl}/purchasing/purchase-order/13/detail`); + await page.getByRole('button', { name: 'Issue Order' }).waitFor(); // Display QR code await page.getByLabel('action-menu-barcode-actions').click(); await page.getByLabel('action-menu-barcode-actions-view').click(); await page.getByRole('img', { name: 'QR Code' }).waitFor(); + await page.getByRole('banner').getByRole('button').click(); + + // Link to barcode + await page.getByLabel('action-menu-barcode-actions').click(); + await page.getByLabel('action-menu-barcode-actions-link-barcode').click(); + await page.getByRole('heading', { name: 'Link Barcode' }).waitFor(); + await page + .getByPlaceholder('Scan barcode data here using') + .fill('1234567890'); + await page.getByRole('button', { name: 'Link' }).click(); + await page.getByRole('button', { name: 'Issue Order' }).waitFor(); + + // Unlink barcode + await page.getByLabel('action-menu-barcode-actions').click(); + await page.getByLabel('action-menu-barcode-actions-unlink-barcode').click(); + await page.getByRole('heading', { name: 'Unlink Barcode' }).waitFor(); + await page.getByText('This will remove the link to').waitFor(); + await page.getByRole('button', { name: 'Unlink Barcode' }).click(); + await page.waitForTimeout(500); + await page.getByRole('button', { name: 'Issue Order' }).waitFor(); });