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();
});