Merge branch 'master' into theme-simplificatio

This commit is contained in:
Oliver Walters
2024-08-23 01:11:22 +00:00
24 changed files with 283 additions and 237 deletions

View File

@ -13,10 +13,11 @@ permissions:
contents: read contents: read
jobs: jobs:
build: synchronize-with-crowdin:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
pull-requests: write
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -39,16 +40,19 @@ jobs:
apt-dependency: gettext apt-dependency: gettext
- name: Make Translations - name: Make Translations
run: invoke translate run: invoke translate
- name: Commit files - name: crowdin action
run: | uses: crowdin/github-action@6ed209d411599a981ccb978df3be9dc9b8a81699 # pin@v2
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git checkout -b l10_local
git add "*.po"
git commit -m "updated translation base"
- name: Push changes
uses: ad-m/github-push-action@d91a481090679876dfc4178fef17f286781251df # pin@v0.8.0
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} upload_sources: true
branch: l10 upload_translations: false
force: true download_translations: true
localization_branch_name: l10_crowdin
create_pull_request: true
pull_request_title: 'New Crowdin updates'
pull_request_body: 'New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)'
pull_request_base_branch_name: 'l10'
pull_request_labels: 'translations'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}

View File

@ -1,12 +1,18 @@
"""InvenTree API version information.""" """InvenTree API version information."""
# InvenTree API version # InvenTree API version
INVENTREE_API_VERSION = 246 INVENTREE_API_VERSION = 248
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" """Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """ INVENTREE_API_TEXT = """
v248 - 2024-08-23 : https://github.com/inventree/InvenTree/pull/7965
- Small adjustments to labels for new custom status fields
v247 - 2024-08-22 : https://github.com/inventree/InvenTree/pull/7956
- Adjust "attachment" field on StockItemTestResult serializer
- Allow null values for attachment
v246 - 2024-08-21 : https://github.com/inventree/InvenTree/pull/7862 v246 - 2024-08-21 : https://github.com/inventree/InvenTree/pull/7862
- Adds custom status fields to various serializers - Adds custom status fields to various serializers

View File

@ -46,7 +46,7 @@ class Migration(migrations.Migration):
models.CharField( models.CharField(
help_text="Label that will be displayed in the frontend", help_text="Label that will be displayed in the frontend",
max_length=250, max_length=250,
verbose_name="label", verbose_name="Label",
), ),
), ),
( (

View File

@ -3355,7 +3355,7 @@ class InvenTreeCustomUserStateModel(models.Model):
) )
label = models.CharField( label = models.CharField(
max_length=250, max_length=250,
verbose_name=_('label'), verbose_name=_('Label'),
help_text=_('Label that will be displayed in the frontend'), help_text=_('Label that will be displayed in the frontend'),
) )
color = models.CharField( color = models.CharField(

View File

@ -239,7 +239,10 @@ class StockItemTestResultSerializer(
) )
attachment = InvenTree.serializers.InvenTreeAttachmentSerializerField( attachment = InvenTree.serializers.InvenTreeAttachmentSerializerField(
required=False required=False,
allow_null=True,
label=_('Attachment'),
help_text=_('Test result attachment'),
) )
def validate(self, data): def validate(self, data):

View File

@ -380,7 +380,7 @@ function stockItemFields(options={}) {
batch: { batch: {
icon: 'fa-layer-group', icon: 'fa-layer-group',
}, },
status_custom_key: {}, status: {},
expiry_date: { expiry_date: {
icon: 'fa-calendar-alt', icon: 'fa-calendar-alt',
}, },

View File

@ -20,7 +20,7 @@ import { ReactNode, useMemo } from 'react';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '../../enums/ModelType';
import { identifierString } from '../../functions/conversion'; import { identifierString } from '../../functions/conversion';
import { InvenTreeIcon } from '../../functions/icons'; import { InvenTreeIcon } from '../../functions/icons';
import { InvenTreeQRCode } from './QRCode'; import { InvenTreeQRCode, QRCodeLink, QRCodeUnlink } from './QRCode';
export type ActionDropdownItem = { export type ActionDropdownItem = {
icon?: ReactNode; icon?: ReactNode;
@ -112,69 +112,91 @@ export function ActionDropdown({
// Dropdown menu for barcode actions // Dropdown menu for barcode actions
export function BarcodeActionDropdown({ export function BarcodeActionDropdown({
actions model,
}: { pk,
actions: ActionDropdownItem[]; 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 ( return (
<ActionDropdown <ActionDropdown
tooltip={t`Barcode Actions`} tooltip={t`Barcode Actions`}
icon={<IconQrcode />} icon={<IconQrcode />}
actions={actions} actions={[
GeneralBarcodeAction({
mdl_prop: prop,
title: t`View`,
icon: <IconQrcode />,
tooltip: t`View barcode`,
ChildItem: InvenTreeQRCode
}),
GeneralBarcodeAction({
hidden: hidden || hash || !permission,
mdl_prop: prop,
title: t`Link Barcode`,
icon: <IconLink />,
tooltip: t`Link a custom barcode to this item`,
ChildItem: QRCodeLink
}),
GeneralBarcodeAction({
hidden: hidden || !hash || !permission,
mdl_prop: prop,
title: t`Unlink Barcode`,
icon: <IconUnlink />,
tooltip: t`Unlink custom barcode`,
ChildItem: QRCodeUnlink
}),
...actions
]}
/> />
); );
} }
// Common action button for viewing a barcode export type QrCodeType = {
export function ViewBarcodeAction({
hidden = false,
model,
pk
}: {
hidden?: boolean;
model: ModelType; model: ModelType;
pk: number; 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 { }): ActionDropdownItem {
const onClick = () => { const onClick = () => {
modals.open({ modals.open({
title: t`View Barcode`, title: title,
children: <InvenTreeQRCode model={model} pk={pk} /> children: <ChildItem mdl_prop={mdl_prop} />
}); });
}; };
return { return {
icon: <IconQrcode />, icon: icon,
name: t`View`, name: title,
tooltip: t`View barcode`, tooltip: tooltip,
onClick: onClick, onClick: onClick,
hidden: hidden hidden: hidden
}; };
} }
// Common action button for linking a custom barcode
export function LinkBarcodeAction(
props: ActionDropdownItem
): ActionDropdownItem {
return {
...props,
icon: <IconLink />,
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: <IconUnlink />,
name: t`Unlink Barcode`,
tooltip: t`Unlink custom barcode`
};
}
// Common action button for editing an item // Common action button for editing an item
export function EditItemAction(props: ActionDropdownItem): ActionDropdownItem { export function EditItemAction(props: ActionDropdownItem): ActionDropdownItem {
return { return {

View File

@ -1,24 +1,28 @@
import { Trans, t } from '@lingui/macro'; import { Trans, t } from '@lingui/macro';
import { import {
Alert,
Box, Box,
Button,
Code, Code,
Group, Group,
Image, Image,
Select, Select,
Skeleton, Skeleton,
Stack, Stack,
Text Text,
TextInput
} from '@mantine/core'; } from '@mantine/core';
import { modals } from '@mantine/modals';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import QR from 'qrcode'; import QR from 'qrcode';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { api } from '../../App'; import { api } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
import { useGlobalSettingsState } from '../../states/SettingsState'; import { useGlobalSettingsState } from '../../states/SettingsState';
import { CopyButton } from '../buttons/CopyButton'; import { CopyButton } from '../buttons/CopyButton';
import { QrCodeType } from './ActionDropdown';
type QRCodeProps = { type QRCodeProps = {
ecl?: 'L' | 'M' | 'Q' | 'H'; ecl?: 'L' | 'M' | 'Q' | 'H';
@ -51,15 +55,13 @@ export const QRCode = ({ data, ecl = 'Q', margin = 1 }: QRCodeProps) => {
}; };
type InvenTreeQRCodeProps = { type InvenTreeQRCodeProps = {
model: ModelType; mdl_prop: QrCodeType;
pk: number;
showEclSelector?: boolean; showEclSelector?: boolean;
} & Omit<QRCodeProps, 'data'>; } & Omit<QRCodeProps, 'data'>;
export const InvenTreeQRCode = ({ export const InvenTreeQRCode = ({
mdl_prop,
showEclSelector = true, showEclSelector = true,
model,
pk,
ecl: eclProp = 'Q', ecl: eclProp = 'Q',
...props ...props
}: InvenTreeQRCodeProps) => { }: InvenTreeQRCodeProps) => {
@ -71,11 +73,11 @@ export const InvenTreeQRCode = ({
}, [eclProp]); }, [eclProp]);
const { data } = useQuery({ const { data } = useQuery({
queryKey: ['qr-code', model, pk], queryKey: ['qr-code', mdl_prop.model, mdl_prop.pk],
queryFn: async () => { queryFn: async () => {
const res = await api.post(apiUrl(ApiEndpoints.generate_barcode), { const res = await api.post(apiUrl(ApiEndpoints.generate_barcode), {
model, model: mdl_prop.model,
pk pk: mdl_prop.pk
}); });
return res.data?.barcode as string; return res.data?.barcode as string;
@ -94,6 +96,15 @@ export const InvenTreeQRCode = ({
return ( return (
<Stack> <Stack>
{mdl_prop.hash ? (
<Alert variant="outline" color="red" title={t`Custom bascode`}>
<Trans>
A custom barcode is registered for this item. The shown code is not
that custom barcode.
</Trans>
</Alert>
) : null}
<QRCode data={data} ecl={ecl} {...props} /> <QRCode data={data} ecl={ecl} {...props} />
{data && settings.getSetting('BARCODE_SHOW_TEXT', 'false') && ( {data && settings.getSetting('BARCODE_SHOW_TEXT', 'false') && (
@ -128,3 +139,55 @@ export const InvenTreeQRCode = ({
</Stack> </Stack>
); );
}; };
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 (
<Box>
<TextInput
label={t`Barcode`}
value={barcode}
onChange={(event) => setBarcode(event.currentTarget.value)}
placeholder={t`Scan barcode data here using barcode scanner`}
/>
<Button color="green" onClick={linkBarcode} mt="lg" fullWidth>
<Trans>Link</Trans>
</Button>
</Box>
);
};
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 (
<Box>
<Text>
<Trans>This will remove the link to the associated barcode</Trans>
</Text>
<Button color="red" onClick={unlinkBarcode}>
<Trans>Unlink Barcode</Trans>
</Button>
</Box>
);
};

View File

@ -39,6 +39,8 @@ export enum ApiEndpoints {
settings_global_list = 'settings/global/', settings_global_list = 'settings/global/',
settings_user_list = 'settings/user/', settings_user_list = 'settings/user/',
barcode = 'barcode/', barcode = 'barcode/',
barcode_link = 'barcode/link/',
barcode_unlink = 'barcode/unlink/',
generate_barcode = 'barcode/generate/', generate_barcode = 'barcode/generate/',
news = 'news/', news = 'news/',
global_status = 'generic/status/', global_status = 'generic/status/',

View File

@ -138,7 +138,7 @@ export function useStockFields({
value: batchCode, value: batchCode,
onValueChange: (value) => setBatchCode(value) onValueChange: (value) => setBatchCode(value)
}, },
status_custom_key: {}, status: {},
expiry_date: { expiry_date: {
// TODO: icon // TODO: icon
}, },
@ -922,10 +922,14 @@ export function stockLocationFields(): ApiFormFieldSet {
// Construct a set of fields for // Construct a set of fields for
export function useTestResultFields({ export function useTestResultFields({
partId, partId,
itemId itemId,
templateId,
editTemplate = false
}: { }: {
partId: number; partId: number;
itemId: number; itemId: number;
templateId: number | undefined;
editTemplate?: boolean;
}): ApiFormFieldSet { }): ApiFormFieldSet {
// Valid field choices // Valid field choices
const [choices, setChoices] = useState<any[]>([]); const [choices, setChoices] = useState<any[]>([]);
@ -947,6 +951,7 @@ export function useTestResultFields({
hidden: true hidden: true
}, },
template: { template: {
disabled: !editTemplate && !!templateId,
filters: { filters: {
include_inherited: true, include_inherited: true,
part: partId part: partId
@ -990,5 +995,13 @@ export function useTestResultFields({
hidden: !includeTestStation hidden: !includeTestStation
} }
}; };
}, [choices, fieldType, partId, itemId, includeTestStation]); }, [
choices,
editTemplate,
fieldType,
partId,
itemId,
templateId,
includeTestStation
]);
} }

View File

@ -36,9 +36,13 @@ export type TableState = {
setRecordCount: (count: number) => void; setRecordCount: (count: number) => void;
page: number; page: number;
setPage: (page: number) => void; setPage: (page: number) => void;
pageSize: number;
setPageSize: (pageSize: number) => void;
records: any[]; records: any[];
setRecords: (records: any[]) => void; setRecords: (records: any[]) => void;
updateRecord: (record: any) => void; updateRecord: (record: any) => void;
editable: boolean;
setEditable: (value: boolean) => void;
}; };
/** /**
@ -97,6 +101,7 @@ export function useTable(tableName: string): TableState {
// Pagination data // Pagination data
const [page, setPage] = useState<number>(1); const [page, setPage] = useState<number>(1);
const [pageSize, setPageSize] = useState<number>(25);
// A list of hidden columns, saved to local storage // A list of hidden columns, saved to local storage
const [hiddenColumns, setHiddenColumns] = useLocalStorage<string[]>({ const [hiddenColumns, setHiddenColumns] = useLocalStorage<string[]>({
@ -131,6 +136,8 @@ export function useTable(tableName: string): TableState {
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const [editable, setEditable] = useState<boolean>(false);
return { return {
tableKey, tableKey,
refreshTable, refreshTable,
@ -154,8 +161,12 @@ export function useTable(tableName: string): TableState {
setRecordCount, setRecordCount,
page, page,
setPage, setPage,
pageSize,
setPageSize,
records, records,
setRecords, setRecords,
updateRecord updateRecord,
editable,
setEditable
}; };
} }

View File

@ -30,10 +30,7 @@ import {
CancelItemAction, CancelItemAction,
DuplicateItemAction, DuplicateItemAction,
EditItemAction, EditItemAction,
HoldItemAction, HoldItemAction
LinkBarcodeAction,
UnlinkBarcodeAction,
ViewBarcodeAction
} from '../../components/items/ActionDropdown'; } from '../../components/items/ActionDropdown';
import InstanceDetail from '../../components/nav/InstanceDetail'; import InstanceDetail from '../../components/nav/InstanceDetail';
import { PageDetail } from '../../components/nav/PageDetail'; import { PageDetail } from '../../components/nav/PageDetail';
@ -43,7 +40,6 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles'; import { UserRoles } from '../../enums/Roles';
import { useBuildOrderFields } from '../../forms/BuildForms'; import { useBuildOrderFields } from '../../forms/BuildForms';
import { notYetImplemented } from '../../functions/notifications';
import { import {
useCreateApiFormModal, useCreateApiFormModal,
useEditApiFormModal useEditApiFormModal
@ -472,20 +468,9 @@ export default function BuildDetail() {
/>, />,
<AdminButton model={ModelType.build} pk={build.pk} />, <AdminButton model={ModelType.build} pk={build.pk} />,
<BarcodeActionDropdown <BarcodeActionDropdown
actions={[ model={ModelType.build}
ViewBarcodeAction({ pk={build.pk}
model: ModelType.build, hash={build?.barcode_hash}
pk: build.pk
}),
LinkBarcodeAction({
hidden: build?.barcode_hash,
onClick: notYetImplemented
}),
UnlinkBarcodeAction({
hidden: !build?.barcode_hash,
onClick: notYetImplemented
})
]}
/>, />,
<PrintingActions <PrintingActions
modelType={ModelType.build} modelType={ModelType.build}

View File

@ -22,10 +22,7 @@ import {
BarcodeActionDropdown, BarcodeActionDropdown,
DeleteItemAction, DeleteItemAction,
DuplicateItemAction, DuplicateItemAction,
EditItemAction, EditItemAction
LinkBarcodeAction,
UnlinkBarcodeAction,
ViewBarcodeAction
} from '../../components/items/ActionDropdown'; } from '../../components/items/ActionDropdown';
import InstanceDetail from '../../components/nav/InstanceDetail'; import InstanceDetail from '../../components/nav/InstanceDetail';
import { PageDetail } from '../../components/nav/PageDetail'; import { PageDetail } from '../../components/nav/PageDetail';
@ -34,7 +31,6 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles'; import { UserRoles } from '../../enums/Roles';
import { useSupplierPartFields } from '../../forms/CompanyForms'; import { useSupplierPartFields } from '../../forms/CompanyForms';
import { notYetImplemented } from '../../functions/notifications';
import { getDetailUrl } from '../../functions/urls'; import { getDetailUrl } from '../../functions/urls';
import { import {
useCreateApiFormModal, useCreateApiFormModal,
@ -271,24 +267,10 @@ export default function SupplierPartDetail() {
return [ return [
<AdminButton model={ModelType.supplierpart} pk={supplierPart.pk} />, <AdminButton model={ModelType.supplierpart} pk={supplierPart.pk} />,
<BarcodeActionDropdown <BarcodeActionDropdown
actions={[ model={ModelType.supplierpart}
ViewBarcodeAction({ pk={supplierPart.pk}
model: ModelType.supplierpart, hash={supplierPart.barcode_hash}
pk: supplierPart.pk perm={user.hasChangeRole(UserRoles.purchase_order)}
}),
LinkBarcodeAction({
hidden:
supplierPart.barcode_hash ||
!user.hasChangeRole(UserRoles.purchase_order),
onClick: notYetImplemented
}),
UnlinkBarcodeAction({
hidden:
!supplierPart.barcode_hash ||
!user.hasChangeRole(UserRoles.purchase_order),
onClick: notYetImplemented
})
]}
/>, />,
<ActionDropdown <ActionDropdown
tooltip={t`Supplier Part Actions`} tooltip={t`Supplier Part Actions`}

View File

@ -51,10 +51,7 @@ import {
BarcodeActionDropdown, BarcodeActionDropdown,
DeleteItemAction, DeleteItemAction,
DuplicateItemAction, DuplicateItemAction,
EditItemAction, EditItemAction
LinkBarcodeAction,
UnlinkBarcodeAction,
ViewBarcodeAction
} from '../../components/items/ActionDropdown'; } from '../../components/items/ActionDropdown';
import { PlaceholderPanel } from '../../components/items/Placeholder'; import { PlaceholderPanel } from '../../components/items/Placeholder';
import { StylishText } from '../../components/items/StylishText'; import { StylishText } from '../../components/items/StylishText';
@ -74,7 +71,6 @@ import {
useTransferStockItem useTransferStockItem
} from '../../forms/StockForms'; } from '../../forms/StockForms';
import { InvenTreeIcon } from '../../functions/icons'; import { InvenTreeIcon } from '../../functions/icons';
import { notYetImplemented } from '../../functions/notifications';
import { getDetailUrl } from '../../functions/urls'; import { getDetailUrl } from '../../functions/urls';
import { import {
useCreateApiFormModal, useCreateApiFormModal,
@ -993,20 +989,10 @@ export default function PartDetail() {
return [ return [
<AdminButton model={ModelType.part} pk={part.pk} />, <AdminButton model={ModelType.part} pk={part.pk} />,
<BarcodeActionDropdown <BarcodeActionDropdown
actions={[ model={ModelType.part}
ViewBarcodeAction({ pk={part.pk}
model: ModelType.part, hash={part?.barcode_hash}
pk: part.pk perm={user.hasChangeRole(UserRoles.part)}
}),
LinkBarcodeAction({
hidden: part?.barcode_hash || !user.hasChangeRole(UserRoles.part),
onClick: notYetImplemented
}),
UnlinkBarcodeAction({
hidden: !part?.barcode_hash || !user.hasChangeRole(UserRoles.part),
onClick: notYetImplemented
})
]}
key="action_dropdown" key="action_dropdown"
/>, />,
<PrintingActions <PrintingActions

View File

@ -24,10 +24,7 @@ import {
CancelItemAction, CancelItemAction,
DuplicateItemAction, DuplicateItemAction,
EditItemAction, EditItemAction,
HoldItemAction, HoldItemAction
LinkBarcodeAction,
UnlinkBarcodeAction,
ViewBarcodeAction
} from '../../components/items/ActionDropdown'; } from '../../components/items/ActionDropdown';
import { StylishText } from '../../components/items/StylishText'; import { StylishText } from '../../components/items/StylishText';
import InstanceDetail from '../../components/nav/InstanceDetail'; import InstanceDetail from '../../components/nav/InstanceDetail';
@ -39,7 +36,6 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles'; import { UserRoles } from '../../enums/Roles';
import { usePurchaseOrderFields } from '../../forms/PurchaseOrderForms'; import { usePurchaseOrderFields } from '../../forms/PurchaseOrderForms';
import { notYetImplemented } from '../../functions/notifications';
import { import {
useCreateApiFormModal, useCreateApiFormModal,
useEditApiFormModal useEditApiFormModal
@ -403,20 +399,9 @@ export default function PurchaseOrderDetail() {
/>, />,
<AdminButton model={ModelType.purchaseorder} pk={order.pk} />, <AdminButton model={ModelType.purchaseorder} pk={order.pk} />,
<BarcodeActionDropdown <BarcodeActionDropdown
actions={[ model={ModelType.purchaseorder}
ViewBarcodeAction({ pk={order.pk}
model: ModelType.purchaseorder, hash={order?.barcode_hash}
pk: order.pk
}),
LinkBarcodeAction({
hidden: order?.barcode_hash,
onClick: notYetImplemented
}),
UnlinkBarcodeAction({
hidden: !order?.barcode_hash,
onClick: notYetImplemented
})
]}
/>, />,
<PrintingActions <PrintingActions
modelType={ModelType.purchaseorder} modelType={ModelType.purchaseorder}

View File

@ -23,10 +23,7 @@ import {
CancelItemAction, CancelItemAction,
DuplicateItemAction, DuplicateItemAction,
EditItemAction, EditItemAction,
HoldItemAction, HoldItemAction
LinkBarcodeAction,
UnlinkBarcodeAction,
ViewBarcodeAction
} from '../../components/items/ActionDropdown'; } from '../../components/items/ActionDropdown';
import { StylishText } from '../../components/items/StylishText'; import { StylishText } from '../../components/items/StylishText';
import InstanceDetail from '../../components/nav/InstanceDetail'; import InstanceDetail from '../../components/nav/InstanceDetail';
@ -38,7 +35,6 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles'; import { UserRoles } from '../../enums/Roles';
import { useReturnOrderFields } from '../../forms/SalesOrderForms'; import { useReturnOrderFields } from '../../forms/SalesOrderForms';
import { notYetImplemented } from '../../functions/notifications';
import { import {
useCreateApiFormModal, useCreateApiFormModal,
useEditApiFormModal useEditApiFormModal
@ -404,20 +400,9 @@ export default function ReturnOrderDetail() {
/>, />,
<AdminButton model={ModelType.returnorder} pk={order.pk} />, <AdminButton model={ModelType.returnorder} pk={order.pk} />,
<BarcodeActionDropdown <BarcodeActionDropdown
actions={[ model={ModelType.returnorder}
ViewBarcodeAction({ pk={order.pk}
model: ModelType.returnorder, hash={order?.barcode_hash}
pk: order.pk
}),
LinkBarcodeAction({
hidden: order?.barcode_hash,
onClick: notYetImplemented
}),
UnlinkBarcodeAction({
hidden: !order?.barcode_hash,
onClick: notYetImplemented
})
]}
/>, />,
<PrintingActions <PrintingActions
modelType={ModelType.returnorder} modelType={ModelType.returnorder}

View File

@ -26,10 +26,7 @@ import {
CancelItemAction, CancelItemAction,
DuplicateItemAction, DuplicateItemAction,
EditItemAction, EditItemAction,
HoldItemAction, HoldItemAction
LinkBarcodeAction,
UnlinkBarcodeAction,
ViewBarcodeAction
} from '../../components/items/ActionDropdown'; } from '../../components/items/ActionDropdown';
import { StylishText } from '../../components/items/StylishText'; import { StylishText } from '../../components/items/StylishText';
import InstanceDetail from '../../components/nav/InstanceDetail'; import InstanceDetail from '../../components/nav/InstanceDetail';
@ -41,7 +38,6 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles'; import { UserRoles } from '../../enums/Roles';
import { useSalesOrderFields } from '../../forms/SalesOrderForms'; import { useSalesOrderFields } from '../../forms/SalesOrderForms';
import { notYetImplemented } from '../../functions/notifications';
import { import {
useCreateApiFormModal, useCreateApiFormModal,
useEditApiFormModal useEditApiFormModal
@ -444,20 +440,9 @@ export default function SalesOrderDetail() {
/>, />,
<AdminButton model={ModelType.salesorder} pk={order.pk} />, <AdminButton model={ModelType.salesorder} pk={order.pk} />,
<BarcodeActionDropdown <BarcodeActionDropdown
actions={[ model={ModelType.salesorder}
ViewBarcodeAction({ pk={order.pk}
model: ModelType.salesorder, hash={order?.barcode_hash}
pk: order.pk
}),
LinkBarcodeAction({
hidden: order?.barcode_hash,
onClick: notYetImplemented
}),
UnlinkBarcodeAction({
hidden: !order?.barcode_hash,
onClick: notYetImplemented
})
]}
/>, />,
<PrintingActions <PrintingActions
modelType={ModelType.salesorder} modelType={ModelType.salesorder}

View File

@ -18,10 +18,7 @@ import {
ActionDropdown, ActionDropdown,
BarcodeActionDropdown, BarcodeActionDropdown,
DeleteItemAction, DeleteItemAction,
EditItemAction, EditItemAction
LinkBarcodeAction,
UnlinkBarcodeAction,
ViewBarcodeAction
} from '../../components/items/ActionDropdown'; } from '../../components/items/ActionDropdown';
import { ApiIcon } from '../../components/items/ApiIcon'; import { ApiIcon } from '../../components/items/ApiIcon';
import InstanceDetail from '../../components/nav/InstanceDetail'; import InstanceDetail from '../../components/nav/InstanceDetail';
@ -287,17 +284,9 @@ export default function Stock() {
/>, />,
location.pk ? ( location.pk ? (
<BarcodeActionDropdown <BarcodeActionDropdown
model={ModelType.stocklocation}
pk={location.pk}
actions={[ actions={[
ViewBarcodeAction({
model: ModelType.stocklocation,
pk: location.pk
}),
LinkBarcodeAction({
onClick: notYetImplemented
}),
UnlinkBarcodeAction({
onClick: notYetImplemented
}),
{ {
name: 'Scan in stock items', name: 'Scan in stock items',
icon: <InvenTreeIcon icon="stock" />, icon: <InvenTreeIcon icon="stock" />,

View File

@ -27,10 +27,7 @@ import {
BarcodeActionDropdown, BarcodeActionDropdown,
DeleteItemAction, DeleteItemAction,
DuplicateItemAction, DuplicateItemAction,
EditItemAction, EditItemAction
LinkBarcodeAction,
UnlinkBarcodeAction,
ViewBarcodeAction
} from '../../components/items/ActionDropdown'; } from '../../components/items/ActionDropdown';
import { StylishText } from '../../components/items/StylishText'; import { StylishText } from '../../components/items/StylishText';
import InstanceDetail from '../../components/nav/InstanceDetail'; import InstanceDetail from '../../components/nav/InstanceDetail';
@ -50,7 +47,6 @@ import {
useTransferStockItem useTransferStockItem
} from '../../forms/StockForms'; } from '../../forms/StockForms';
import { InvenTreeIcon } from '../../functions/icons'; import { InvenTreeIcon } from '../../functions/icons';
import { notYetImplemented } from '../../functions/notifications';
import { getDetailUrl } from '../../functions/urls'; import { getDetailUrl } from '../../functions/urls';
import { import {
useCreateApiFormModal, useCreateApiFormModal,
@ -477,22 +473,10 @@ export default function StockDetail() {
() => [ () => [
<AdminButton model={ModelType.stockitem} pk={stockitem.pk} />, <AdminButton model={ModelType.stockitem} pk={stockitem.pk} />,
<BarcodeActionDropdown <BarcodeActionDropdown
actions={[ model={ModelType.stockitem}
ViewBarcodeAction({ pk={stockitem.pk}
model: ModelType.stockitem, hash={stockitem?.barcode_hash}
pk: stockitem.pk perm={user.hasChangeRole(UserRoles.stock)}
}),
LinkBarcodeAction({
hidden:
stockitem?.barcode_hash || !user.hasChangeRole(UserRoles.stock),
onClick: notYetImplemented
}),
UnlinkBarcodeAction({
hidden:
!stockitem?.barcode_hash || !user.hasChangeRole(UserRoles.stock),
onClick: notYetImplemented
})
]}
/>, />,
<PrintingActions <PrintingActions
modelType={ModelType.stockitem} modelType={ModelType.stockitem}

View File

@ -1,3 +1,5 @@
import { ApiFormFieldType } from '../components/forms/fields/ApiFormField';
export type TableColumnProps<T = any> = { export type TableColumnProps<T = any> = {
accessor?: string; // The key in the record to access accessor?: string; // The key in the record to access
title?: string; // The title of the column - Note: this may be supplied by the API, and is not required, but it can be overridden if required title?: string; // The title of the column - Note: this may be supplied by the API, and is not required, but it can be overridden if required
@ -5,6 +7,8 @@ export type TableColumnProps<T = any> = {
sortable?: boolean; // Whether the column is sortable sortable?: boolean; // Whether the column is sortable
switchable?: boolean; // Whether the column is switchable switchable?: boolean; // Whether the column is switchable
hidden?: boolean; // Whether the column is hidden hidden?: boolean; // Whether the column is hidden
editable?: boolean; // Whether the value of this column can be edited
definition?: ApiFormFieldType; // Optional field definition for the column
render?: (record: T, index?: number) => any; // A custom render function render?: (record: T, index?: number) => any; // A custom render function
filter?: any; // A custom filter function filter?: any; // A custom filter function
filtering?: boolean; // Whether the column is filterable filtering?: boolean; // Whether the column is filterable

View File

@ -55,6 +55,7 @@ import { RowAction, RowActions } from './RowActions';
import { TableSearchInput } from './Search'; import { TableSearchInput } from './Search';
const defaultPageSize: number = 25; const defaultPageSize: number = 25;
const PAGE_SIZES = [10, 15, 20, 25, 50, 100, 500];
/** /**
* Set of optional properties which can be passed to an InvenTreeTable component * Set of optional properties which can be passed to an InvenTreeTable component
@ -74,7 +75,6 @@ const defaultPageSize: number = 25;
* @param enableRefresh : boolean - Enable refresh actions * @param enableRefresh : boolean - Enable refresh actions
* @param enableColumnSwitching : boolean - Enable column switching * @param enableColumnSwitching : boolean - Enable column switching
* @param enableColumnCaching : boolean - Enable caching of column names via API * @param enableColumnCaching : boolean - Enable caching of column names via API
* @param pageSize : number - Number of records per page
* @param barcodeActions : any[] - List of barcode actions * @param barcodeActions : any[] - List of barcode actions
* @param tableFilters : TableFilter[] - List of custom filters * @param tableFilters : TableFilter[] - List of custom filters
* @param tableActions : any[] - List of custom action groups * @param tableActions : any[] - List of custom action groups
@ -100,7 +100,6 @@ export type InvenTreeTableProps<T = any> = {
enableLabels?: boolean; enableLabels?: boolean;
enableReports?: boolean; enableReports?: boolean;
afterBulkDelete?: () => void; afterBulkDelete?: () => void;
pageSize?: number;
barcodeActions?: React.ReactNode[]; barcodeActions?: React.ReactNode[];
tableFilters?: TableFilter[]; tableFilters?: TableFilter[];
tableActions?: React.ReactNode[]; tableActions?: React.ReactNode[];
@ -129,7 +128,6 @@ const defaultInvenTreeTableProps: InvenTreeTableProps = {
enableRefresh: true, enableRefresh: true,
enableSearch: true, enableSearch: true,
enableSelection: false, enableSelection: false,
pageSize: defaultPageSize,
defaultSortColumn: '', defaultSortColumn: '',
barcodeActions: [], barcodeActions: [],
tableFilters: [], tableFilters: [],
@ -362,7 +360,8 @@ export function InvenTreeTable<T = any>({
// Pagination // Pagination
if (tableProps.enablePagination && paginate) { if (tableProps.enablePagination && paginate) {
let pageSize = tableProps.pageSize ?? defaultPageSize; let pageSize = tableState.pageSize ?? defaultPageSize;
if (pageSize != tableState.pageSize) tableState.setPageSize(pageSize);
queryParams.limit = pageSize; queryParams.limit = pageSize;
queryParams.offset = (tableState.page - 1) * pageSize; queryParams.offset = (tableState.page - 1) * pageSize;
} }
@ -590,6 +589,12 @@ export function InvenTreeTable<T = any>({
[props.onRowClick, props.onCellClick] [props.onRowClick, props.onCellClick]
); );
// pagination refresth table if pageSize changes
function updatePageSize(newData: number) {
tableState.setPageSize(newData);
tableState.refreshTable();
}
return ( return (
<> <>
{deleteRecords.modal} {deleteRecords.modal}
@ -699,6 +704,7 @@ export function InvenTreeTable<T = any>({
<DataTable <DataTable
withTableBorder withTableBorder
withColumnBorders
striped striped
highlightOnHover highlightOnHover
loaderType={loader} loaderType={loader}
@ -706,7 +712,7 @@ export function InvenTreeTable<T = any>({
idAccessor={tableProps.idAccessor} idAccessor={tableProps.idAccessor}
minHeight={300} minHeight={300}
totalRecords={tableState.recordCount} totalRecords={tableState.recordCount}
recordsPerPage={tableProps.pageSize ?? defaultPageSize} recordsPerPage={tableState.pageSize}
page={tableState.page} page={tableState.page}
onPageChange={tableState.setPage} onPageChange={tableState.setPage}
sortStatus={sortStatus} sortStatus={sortStatus}
@ -734,6 +740,8 @@ export function InvenTreeTable<T = any>({
overflow: 'hidden' overflow: 'hidden'
}) })
}} }}
recordsPerPageOptions={PAGE_SIZES}
onRecordsPerPageChange={updatePageSize}
/> />
</Box> </Box>
</Stack> </Stack>

View File

@ -67,7 +67,8 @@ export default function BuildOrderTestTable({
const testResultFields: ApiFormFieldSet = useTestResultFields({ const testResultFields: ApiFormFieldSet = useTestResultFields({
partId: partId, partId: partId,
itemId: selectedOutput itemId: selectedOutput,
templateId: selectedTemplate
}); });
const createTestResult = useCreateApiFormModal({ const createTestResult = useCreateApiFormModal({

View File

@ -238,15 +238,16 @@ export default function StockItemTestResultTable({
]; ];
}, [itemId]); }, [itemId]);
const resultFields: ApiFormFieldSet = useTestResultFields({
partId: partId,
itemId: itemId
});
const [selectedTemplate, setSelectedTemplate] = useState<number | undefined>( const [selectedTemplate, setSelectedTemplate] = useState<number | undefined>(
undefined undefined
); );
const resultFields: ApiFormFieldSet = useTestResultFields({
partId: partId,
itemId: itemId,
templateId: selectedTemplate
});
const newTestModal = useCreateApiFormModal({ const newTestModal = useCreateApiFormModal({
url: ApiEndpoints.stock_test_result_list, url: ApiEndpoints.stock_test_result_list,
fields: useMemo(() => ({ ...resultFields }), [resultFields]), fields: useMemo(() => ({ ...resultFields }), [resultFields]),

View File

@ -59,9 +59,36 @@ test('PUI - Purchase Orders', async ({ page }) => {
await page.getByRole('cell', { name: 'PO0013' }).click(); await page.getByRole('cell', { name: 'PO0013' }).click();
await page.getByRole('button', { name: 'Issue Order' }).waitFor(); 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 // Display QR code
await page.getByLabel('action-menu-barcode-actions').click(); await page.getByLabel('action-menu-barcode-actions').click();
await page.getByLabel('action-menu-barcode-actions-view').click(); await page.getByLabel('action-menu-barcode-actions-view').click();
await page.getByRole('img', { name: 'QR Code' }).waitFor(); 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();
}); });