2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-08-09 21:30:54 +00:00

Merge branch 'master' of https://github.com/inventree/InvenTree into pui-maintine-v7

This commit is contained in:
Matthias Mair
2024-04-30 12:44:53 +02:00
43 changed files with 377 additions and 132 deletions

View File

@@ -1,11 +1,14 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 192
INVENTREE_API_VERSION = 193
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v193 - 2024-04-30 : https://github.com/inventree/InvenTree/pull/7144
- Adds "assigned_to" filter to PurchaseOrder / SalesOrder / ReturnOrder API endpoints
v192 - 2024-04-23 : https://github.com/inventree/InvenTree/pull/7106
- Adds 'trackable' ordering option to BuildLineLabel API endpoint

View File

@@ -148,6 +148,10 @@ class OrderFilter(rest_filters.FilterSet):
return queryset.exclude(project_code=None)
return queryset.filter(project_code=None)
assigned_to = rest_filters.ModelChoiceFilter(
queryset=Owner.objects.all(), field_name='responsible'
)
class LineItemFilter(rest_filters.FilterSet):
"""Base class for custom API filters for order line item list(s)."""

View File

@@ -77,16 +77,18 @@ class AbstractOrderSerializer(serializers.Serializer):
"""Abstract serializer class which provides fields common to all order types."""
# Number of line items in this order
line_items = serializers.IntegerField(read_only=True)
line_items = serializers.IntegerField(read_only=True, label=_('Line Items'))
# Number of completed line items (this is an annotated field)
completed_lines = serializers.IntegerField(read_only=True)
completed_lines = serializers.IntegerField(
read_only=True, label=_('Completed Lines')
)
# Human-readable status text (read-only)
status_text = serializers.CharField(source='get_status_display', read_only=True)
# status field cannot be set directly
status = serializers.IntegerField(read_only=True)
status = serializers.IntegerField(read_only=True, label=_('Order Status'))
# Reference string is *required*
reference = serializers.CharField(required=True)
@@ -114,7 +116,9 @@ class AbstractOrderSerializer(serializers.Serializer):
barcode_hash = serializers.CharField(read_only=True)
creation_date = serializers.DateField(required=False, allow_null=True)
creation_date = serializers.DateField(
required=False, allow_null=True, label=_('Creation Date')
)
def validate_reference(self, reference):
"""Custom validation for the reference field."""

View File

@@ -5,6 +5,6 @@ import { ActionButton, ActionButtonProps } from './ActionButton';
/**
* A generic icon button which is used to add or create a new item
*/
export function AddItemButton(props: ActionButtonProps) {
export function AddItemButton(props: Readonly<ActionButtonProps>) {
return <ActionButton {...props} color="green" icon={<IconPlus />} />;
}

View File

@@ -36,7 +36,7 @@ export function SplitButton({
selected,
setSelected,
loading
}: SplitButtonProps) {
}: Readonly<SplitButtonProps>) {
const [current, setCurrent] = useState<string>(defaultSelected);
useEffect(() => {

View File

@@ -180,7 +180,7 @@ function NameBadge({ pk, type }: { pk: string | number; type: BadgeType }) {
* If owner is defined, only renders a badge
* If user is defined, a badge is rendered in addition to main value
*/
function TableStringValue(props: FieldProps) {
function TableStringValue(props: Readonly<FieldProps>) {
let value = props?.field_value;
if (value === undefined) {
@@ -217,11 +217,11 @@ function TableStringValue(props: FieldProps) {
);
}
function BooleanValue(props: FieldProps) {
function BooleanValue(props: Readonly<FieldProps>) {
return <YesNoButton value={props.field_value} />;
}
function TableAnchorValue(props: FieldProps) {
function TableAnchorValue(props: Readonly<FieldProps>) {
if (props.field_data.external) {
return (
<Anchor
@@ -303,7 +303,7 @@ function TableAnchorValue(props: FieldProps) {
);
}
function ProgressBarValue(props: FieldProps) {
function ProgressBarValue(props: Readonly<FieldProps>) {
return (
<ProgressBar
value={props.field_data.progress}
@@ -313,7 +313,7 @@ function ProgressBarValue(props: FieldProps) {
);
}
function StatusValue(props: FieldProps) {
function StatusValue(props: Readonly<FieldProps>) {
return (
<StatusRenderer type={props.field_data.model} status={props.field_value} />
);

View File

@@ -7,7 +7,7 @@ export type DetailsBadgeProps = {
visible?: boolean;
};
export default function DetailsBadge(props: DetailsBadgeProps) {
export default function DetailsBadge(props: Readonly<DetailsBadgeProps>) {
if (props.visible == false) {
return null;
}

View File

@@ -322,7 +322,7 @@ function ImageActionButtons({
/**
* Renders an image with action buttons for display on Details panels
*/
export function DetailsImage(props: DetailImageProps) {
export function DetailsImage(props: Readonly<DetailImageProps>) {
// Displays a group of ActionButtons on hover
const { hovered, ref } = useHover();
const [img, setImg] = useState<string>(props.src ?? backup_image);

View File

@@ -87,7 +87,7 @@ type TemplateEditorProps = {
template: TemplateI;
};
export function TemplateEditor(props: TemplateEditorProps) {
export function TemplateEditor(props: Readonly<TemplateEditorProps>) {
const { downloadUrl, editors, previewAreas, preview } = props;
const editorRef = useRef<EditorRef>();
const previewRef = useRef<PreviewAreaRef>();

View File

@@ -6,7 +6,12 @@ interface DocInfoProps extends BaseDocProps {
size?: number;
}
export function DocInfo({ size = 18, text, detail, link }: DocInfoProps) {
export function DocInfo({
size = 18,
text,
detail,
link
}: Readonly<DocInfoProps>) {
return (
<DocTooltip text={text} detail={detail} link={link}>
<IconInfoCircle size={size} />

View File

@@ -21,7 +21,7 @@ export function DocTooltip({
detail,
link,
docchildren
}: DocTooltipProps) {
}: Readonly<DocTooltipProps>) {
return (
<HoverCard
shadow="md"

View File

@@ -6,13 +6,14 @@ export type ProgressBarProps = {
maximum?: number;
label?: string;
progressLabel?: boolean;
size?: string;
};
/**
* A progress bar element, built on mantine.Progress
* The color of the bar is determined based on the value
*/
export function ProgressBar(props: ProgressBarProps) {
export function ProgressBar(props: Readonly<ProgressBarProps>) {
const progress = useMemo(() => {
let maximum = props.maximum ?? 100;
let value = Math.max(props.value, 0);
@@ -31,8 +32,8 @@ export function ProgressBar(props: ProgressBarProps) {
<Progress
value={progress}
color={progress < 100 ? 'orange' : progress > 100 ? 'blue' : 'green'}
size="sm"
radius="xs"
size={props.size ?? 'md'}
radius="sm"
/>
</Stack>
);

View File

@@ -14,7 +14,7 @@ export function TitleWithDoc({
size,
text,
detail
}: DocTitleProps) {
}: Readonly<DocTitleProps>) {
return (
<Group>
<Title variant={variant} order={order} size={size}>

View File

@@ -29,7 +29,7 @@ function DetailDrawerComponent({
size,
closeOnEscape = true,
renderContent
}: DrawerProps) {
}: Readonly<DrawerProps>) {
const navigate = useNavigate();
const { id } = useParams();
@@ -80,7 +80,7 @@ function DetailDrawerComponent({
);
}
export function DetailDrawer(props: DrawerProps) {
export function DetailDrawer(props: Readonly<DrawerProps>) {
return (
<Routes>
<Route path=":id?/" element={<DetailDrawerComponent {...props} />} />

View File

@@ -5,6 +5,17 @@ import { ApiImage } from '../images/ApiImage';
import { StylishText } from '../items/StylishText';
import { Breadcrumb, BreadcrumbList } from './BreadcrumbList';
interface PageDetailInterface {
title?: string;
subtitle?: string;
imageUrl?: string;
detail?: ReactNode;
badges?: ReactNode[];
breadcrumbs?: Breadcrumb[];
breadcrumbAction?: () => void;
actions?: ReactNode[];
}
/**
* Construct a "standard" page detail for common display between pages.
*
@@ -20,16 +31,7 @@ export function PageDetail({
breadcrumbs,
breadcrumbAction,
actions
}: {
title?: string;
subtitle?: string;
imageUrl?: string;
detail?: ReactNode;
badges?: ReactNode[];
breadcrumbs?: Breadcrumb[];
breadcrumbAction?: () => void;
actions?: ReactNode[];
}) {
}: Readonly<PageDetailInterface>) {
return (
<Stack gap="xs">
{breadcrumbs && breadcrumbs.length > 0 && (

View File

@@ -50,7 +50,7 @@ function BasePanelGroup({
onPanelChange,
selectedPanel,
collapsible = true
}: PanelProps): ReactNode {
}: Readonly<PanelProps>): ReactNode {
const navigate = useNavigate();
const { panel } = useParams();
@@ -178,7 +178,11 @@ function BasePanelGroup({
);
}
function IndexPanelComponent({ pageKey, selectedPanel, panels }: PanelProps) {
function IndexPanelComponent({
pageKey,
selectedPanel,
panels
}: Readonly<PanelProps>) {
const lastUsedPanel = useLocalState((state) => {
const panelName =
selectedPanel || state.lastUsedPanels[pageKey] || panels[0]?.name;
@@ -203,7 +207,7 @@ function IndexPanelComponent({ pageKey, selectedPanel, panels }: PanelProps) {
* @param onPanelChange - Callback when the active panel changes
* @param collapsible - If true, the panel group can be collapsed (defaults to true)
*/
export function PanelGroup(props: PanelProps) {
export function PanelGroup(props: Readonly<PanelProps>) {
return (
<Routes>
<Route index element={<IndexPanelComponent {...props} />} />

View File

@@ -3,6 +3,15 @@ import { IconSwitch } from '@tabler/icons-react';
import { ReactNode } from 'react';
import { Link } from 'react-router-dom';
interface SettingsHeaderInterface {
title: string | ReactNode;
shorthand?: string;
subtitle?: string | ReactNode;
switch_condition?: boolean;
switch_text?: string | ReactNode;
switch_link?: string;
}
/**
* Construct a settings page header with interlinks to one other settings page
*/
@@ -13,14 +22,7 @@ export function SettingsHeader({
switch_condition = true,
switch_text,
switch_link
}: {
title: string | ReactNode;
shorthand?: string;
subtitle?: string | ReactNode;
switch_condition?: boolean;
switch_text?: string | ReactNode;
switch_link?: string;
}) {
}: Readonly<SettingsHeaderInterface>) {
return (
<Stack gap="0" ml={'sm'}>
<Group>

View File

@@ -1,13 +1,15 @@
import { ReactNode } from 'react';
import { ModelType } from '../../enums/ModelType';
import { RenderInlineModel } from './Instance';
import { InstanceRenderInterface, RenderInlineModel } from './Instance';
import { StatusRenderer } from './StatusRenderer';
/**
* Inline rendering of a single BuildOrder instance
*/
export function RenderBuildOrder({ instance }: { instance: any }): ReactNode {
export function RenderBuildOrder({
instance
}: Readonly<InstanceRenderInterface>): ReactNode {
return (
<RenderInlineModel
primary={instance.reference}
@@ -24,7 +26,9 @@ export function RenderBuildOrder({ instance }: { instance: any }): ReactNode {
/*
* Inline rendering of a single BuildLine instance
*/
export function RenderBuildLine({ instance }: { instance: any }): ReactNode {
export function RenderBuildLine({
instance
}: Readonly<InstanceRenderInterface>): ReactNode {
return (
<RenderInlineModel
primary={instance.part_detail.full_name}

View File

@@ -1,11 +1,13 @@
import { ReactNode } from 'react';
import { RenderInlineModel } from './Instance';
import { InstanceRenderInterface, RenderInlineModel } from './Instance';
/**
* Inline rendering of a single Address instance
*/
export function RenderAddress({ instance }: { instance: any }): ReactNode {
export function RenderAddress({
instance
}: Readonly<InstanceRenderInterface>): ReactNode {
let text = [
instance.country,
instance.postal_code,
@@ -23,7 +25,9 @@ export function RenderAddress({ instance }: { instance: any }): ReactNode {
/**
* Inline rendering of a single Company instance
*/
export function RenderCompany({ instance }: { instance: any }): ReactNode {
export function RenderCompany({
instance
}: Readonly<InstanceRenderInterface>): ReactNode {
// TODO: Handle URL
return (
@@ -38,14 +42,18 @@ export function RenderCompany({ instance }: { instance: any }): ReactNode {
/**
* Inline rendering of a single Contact instance
*/
export function RenderContact({ instance }: { instance: any }): ReactNode {
export function RenderContact({
instance
}: Readonly<InstanceRenderInterface>): ReactNode {
return <RenderInlineModel primary={instance.name} />;
}
/**
* Inline rendering of a single SupplierPart instance
*/
export function RenderSupplierPart({ instance }: { instance: any }): ReactNode {
export function RenderSupplierPart({
instance
}: Readonly<InstanceRenderInterface>): ReactNode {
// TODO: handle URL
let supplier = instance.supplier_detail ?? {};
@@ -66,9 +74,7 @@ export function RenderSupplierPart({ instance }: { instance: any }): ReactNode {
*/
export function RenderManufacturerPart({
instance
}: {
instance: any;
}): ReactNode {
}: Readonly<InstanceRenderInterface>): ReactNode {
let part = instance.part_detail ?? {};
let manufacturer = instance.manufacturer_detail ?? {};

View File

@@ -1,8 +1,10 @@
import { ReactNode } from 'react';
import { RenderInlineModel } from './Instance';
import { InstanceRenderInterface, RenderInlineModel } from './Instance';
export function RenderProjectCode({ instance }: { instance: any }): ReactNode {
export function RenderProjectCode({
instance
}: Readonly<InstanceRenderInterface>): ReactNode {
return (
instance && (
<RenderInlineModel

View File

@@ -37,7 +37,7 @@ type EnumDictionary<T extends string | symbol | number, U> = {
*/
const RendererLookup: EnumDictionary<
ModelType,
(props: { instance: any }) => ReactNode
(props: Readonly<InstanceRenderInterface>) => ReactNode
> = {
[ModelType.address]: RenderAddress,
[ModelType.build]: RenderBuildOrder,
@@ -139,3 +139,7 @@ export function UnknownRenderer({
</Alert>
);
}
export interface InstanceRenderInterface {
instance: any;
}

View File

@@ -2,7 +2,7 @@ import { t } from '@lingui/macro';
import { ReactNode } from 'react';
import { ModelType } from '../../enums/ModelType';
import { RenderInlineModel } from './Instance';
import { InstanceRenderInterface, RenderInlineModel } from './Instance';
import { StatusRenderer } from './StatusRenderer';
/**
@@ -10,9 +10,7 @@ import { StatusRenderer } from './StatusRenderer';
*/
export function RenderPurchaseOrder({
instance
}: {
instance: any;
}): ReactNode {
}: Readonly<InstanceRenderInterface>): ReactNode {
let supplier = instance.supplier_detail || {};
// TODO: Handle URL
@@ -32,7 +30,9 @@ export function RenderPurchaseOrder({
/**
* Inline rendering of a single ReturnOrder instance
*/
export function RenderReturnOrder({ instance }: { instance: any }): ReactNode {
export function RenderReturnOrder({
instance
}: Readonly<InstanceRenderInterface>): ReactNode {
let customer = instance.customer_detail || {};
return (
@@ -51,7 +51,9 @@ export function RenderReturnOrder({ instance }: { instance: any }): ReactNode {
/**
* Inline rendering of a single SalesOrder instance
*/
export function RenderSalesOrder({ instance }: { instance: any }): ReactNode {
export function RenderSalesOrder({
instance
}: Readonly<InstanceRenderInterface>): ReactNode {
let customer = instance.customer_detail || {};
// TODO: Handle URL

View File

@@ -1,12 +1,14 @@
import { t } from '@lingui/macro';
import { ReactNode } from 'react';
import { RenderInlineModel } from './Instance';
import { InstanceRenderInterface, RenderInlineModel } from './Instance';
/**
* Inline rendering of a single Part instance
*/
export function RenderPart({ instance }: { instance: any }): ReactNode {
export function RenderPart({
instance
}: Readonly<InstanceRenderInterface>): ReactNode {
const stock = t`Stock` + `: ${instance.in_stock}`;
return (
@@ -22,7 +24,9 @@ export function RenderPart({ instance }: { instance: any }): ReactNode {
/**
* Inline rendering of a PartCategory instance
*/
export function RenderPartCategory({ instance }: { instance: any }): ReactNode {
export function RenderPartCategory({
instance
}: Readonly<InstanceRenderInterface>): ReactNode {
// TODO: Handle URL
let lvl = '-'.repeat(instance.level || 0);

View File

@@ -14,7 +14,7 @@ export interface StatusCodeListInterface {
[key: string]: StatusCodeInterface;
}
interface renderStatusLabelOptionsInterface {
interface RenderStatusLabelOptionsInterface {
size?: MantineSize;
}
@@ -24,7 +24,7 @@ interface renderStatusLabelOptionsInterface {
function renderStatusLabel(
key: string | number,
codes: StatusCodeListInterface,
options: renderStatusLabelOptionsInterface = {}
options: RenderStatusLabelOptionsInterface = {}
) {
let text = null;
let color = null;
@@ -70,7 +70,7 @@ export const StatusRenderer = ({
}: {
status: string | number;
type: ModelType | string;
options?: renderStatusLabelOptionsInterface;
options?: RenderStatusLabelOptionsInterface;
}) => {
const statusCodeList = useGlobalStatusState.getState().status;

View File

@@ -1,16 +1,14 @@
import { t } from '@lingui/macro';
import { ReactNode } from 'react';
import { RenderInlineModel } from './Instance';
import { InstanceRenderInterface, RenderInlineModel } from './Instance';
/**
* Inline rendering of a single StockLocation instance
*/
export function RenderStockLocation({
instance
}: {
instance: any;
}): ReactNode {
}: Readonly<InstanceRenderInterface>): ReactNode {
return (
<RenderInlineModel
primary={instance.name}
@@ -19,7 +17,9 @@ export function RenderStockLocation({
);
}
export function RenderStockItem({ instance }: { instance: any }): ReactNode {
export function RenderStockItem({
instance
}: Readonly<InstanceRenderInterface>): ReactNode {
let quantity_string = '';
if (instance?.serial !== null && instance?.serial !== undefined) {

View File

@@ -1,9 +1,11 @@
import { IconUser, IconUsersGroup } from '@tabler/icons-react';
import { ReactNode } from 'react';
import { RenderInlineModel } from './Instance';
import { InstanceRenderInterface, RenderInlineModel } from './Instance';
export function RenderOwner({ instance }: { instance: any }): ReactNode {
export function RenderOwner({
instance
}: Readonly<InstanceRenderInterface>): ReactNode {
return (
instance && (
<RenderInlineModel
@@ -14,7 +16,9 @@ export function RenderOwner({ instance }: { instance: any }): ReactNode {
);
}
export function RenderUser({ instance }: { instance: any }): ReactNode {
export function RenderUser({
instance
}: Readonly<InstanceRenderInterface>): ReactNode {
return (
instance && (
<RenderInlineModel

View File

@@ -6,13 +6,13 @@ import {
useUserSettingsState
} from '../states/SettingsState';
interface formatDecmimalOptionsType {
interface FormatDecmimalOptionsInterface {
digits?: number;
minDigits?: number;
locale?: string;
}
interface formatCurrencyOptionsType {
interface FormatCurrencyOptionsInterface {
digits?: number;
minDigits?: number;
currency?: string;
@@ -22,7 +22,7 @@ interface formatCurrencyOptionsType {
export function formatDecimal(
value: number | null | undefined,
options: formatDecmimalOptionsType = {}
options: FormatDecmimalOptionsInterface = {}
) {
let locale = options.locale || navigator.language || 'en-US';
@@ -45,7 +45,7 @@ export function formatDecimal(
*/
export function formatCurrency(
value: number | string | null | undefined,
options: formatCurrencyOptionsType = {}
options: FormatCurrencyOptionsInterface = {}
) {
if (value == null || value == undefined) {
return null;
@@ -90,7 +90,7 @@ export function formatCurrency(
export function formatPriceRange(
minValue: number | null,
maxValue: number | null,
options: formatCurrencyOptionsType = {}
options: FormatCurrencyOptionsInterface = {}
) {
// If neither values are provided, return a dash
if (minValue == null && maxValue == null) {
@@ -117,7 +117,7 @@ export function formatPriceRange(
)}`;
}
interface renderDateOptionsType {
interface RenderDateOptionsInterface {
showTime?: boolean;
showSeconds?: boolean;
}
@@ -128,7 +128,10 @@ interface renderDateOptionsType {
* The provided "date" variable is a string, nominally ISO format e.g. 2022-02-22
* The user-configured setting DATE_DISPLAY_FORMAT determines how the date should be displayed.
*/
export function renderDate(date: string, options: renderDateOptionsType = {}) {
export function renderDate(
date: string,
options: RenderDateOptionsInterface = {}
) {
if (!date) {
return '-';
}

View File

@@ -217,7 +217,7 @@ type InvenTreeIconProps = {
iconProps?: TablerIconProps;
};
export function InvenTreeIcon(props: InvenTreeIconProps) {
export function InvenTreeIcon(props: Readonly<InvenTreeIconProps>) {
let Icon: React.ForwardRefExoticComponent<React.RefAttributes<any>>;
if (props.icon in icons) {

View File

@@ -0,0 +1,91 @@
/*
* Custom hook for retrieving a list of items from the API,
* and turning them into "filters" for use in the frontend table framework.
*/
import { useQuery } from '@tanstack/react-query';
import { useCallback, useMemo } from 'react';
import { api } from '../App';
import { ApiEndpoints } from '../enums/ApiEndpoints';
import { resolveItem } from '../functions/conversion';
import { apiUrl } from '../states/ApiState';
import { TableFilterChoice } from '../tables/Filter';
type UseFilterProps = {
url: string;
method?: 'GET' | 'POST' | 'OPTIONS';
params?: any;
accessor?: string;
transform: (item: any) => TableFilterChoice;
};
export function useFilters(props: UseFilterProps) {
const query = useQuery({
enabled: true,
queryKey: [props.url, props.method, props.params],
queryFn: async () => {
return await api
.request({
url: props.url,
method: props.method || 'GET',
params: props.params
})
.then((response) => {
let data = resolveItem(response, props.accessor ?? 'data');
if (data == null || data == undefined) {
return [];
}
return data;
})
.catch((error) => []);
}
});
const choices: TableFilterChoice[] = useMemo(() => {
return query.data?.map(props.transform) ?? [];
}, [props.transform, query.data]);
const refresh = useCallback(() => {
query.refetch();
}, []);
return {
choices,
refresh
};
}
// Provide list of project code filters
export function useProjectCodeFilters() {
return useFilters({
url: apiUrl(ApiEndpoints.project_code_list),
transform: (item) => ({
value: item.pk,
label: item.code
})
});
}
// Provide list of user filters
export function useUserFilters() {
return useFilters({
url: apiUrl(ApiEndpoints.user_list),
transform: (item) => ({
value: item.pk,
label: item.username
})
});
}
// Provide list of owner filters
export function useOwnerFilters() {
return useFilters({
url: apiUrl(ApiEndpoints.owner_list),
transform: (item) => ({
value: item.pk,
label: item.name
})
});
}

View File

@@ -4,6 +4,7 @@ import {
Badge,
Button,
Checkbox,
Col,
Container,
Grid,
Group,
@@ -479,11 +480,11 @@ enum InputMethod {
ImageBarcode = 'imageBarcode'
}
interface inputProps {
interface ScanInputInterface {
action: (items: ScanItem[]) => void;
}
function InputManual({ action }: inputProps) {
function InputManual({ action }: Readonly<ScanInputInterface>) {
const [value, setValue] = useState<string>('');
function btnAddItem() {
@@ -535,7 +536,7 @@ function InputManual({ action }: inputProps) {
}
/* Input that uses QR code detection from images */
function InputImageBarcode({ action }: inputProps) {
function InputImageBarcode({ action }: Readonly<ScanInputInterface>) {
const [qrCodeScanner, setQrCodeScanner] = useState<Html5Qrcode | null>(null);
const [camId, setCamId] = useLocalStorage<CameraDevice | null>({
key: 'camId',

View File

@@ -56,7 +56,7 @@ export type CompanyDetailProps = {
/**
* Detail view for a single company instance
*/
export default function CompanyDetail(props: CompanyDetailProps) {
export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
const { id } = useParams();
const user = useUserState();

View File

@@ -104,7 +104,7 @@ export function RowActions({
}, [actions]);
// Render a single action icon
function RowActionIcon(action: RowAction) {
function RowActionIcon(action: Readonly<RowAction>) {
return (
<Tooltip
withinPortal={true}

View File

@@ -10,6 +10,11 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { useBuildOrderFields } from '../../forms/BuildForms';
import {
useOwnerFilters,
useProjectCodeFilters,
useUserFilters
} from '../../hooks/UseFilter';
import { useCreateApiFormModal } from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
@@ -92,6 +97,10 @@ export function BuildOrderTable({
}) {
const tableColumns = useMemo(() => buildOrderTableColumns(), []);
const projectCodeFilters = useProjectCodeFilters();
const userFilters = useUserFilters();
const responsibleFilters = useOwnerFilters();
const tableFilters: TableFilter[] = useMemo(() => {
return [
{
@@ -117,18 +126,36 @@ export function BuildOrderTable({
type: 'boolean',
label: t`Assigned to me`,
description: t`Show orders assigned to me`
},
{
name: 'project_code',
label: t`Project Code`,
description: t`Filter by project code`,
choices: projectCodeFilters.choices
},
{
name: 'has_project_code',
label: t`Has Project Code`,
description: t`Filter by whether the purchase order has a project code`
},
{
name: 'issued_by',
label: t`Issued By`,
description: t`Filter by user who issued this order`,
choices: userFilters.choices
},
{
name: 'assigned_to',
label: t`Responsible`,
description: t`Filter by responsible owner`,
choices: responsibleFilters.choices
}
// TODO: 'assigned to' filter
// TODO: 'issued by' filter
// {
// name: 'has_project_code',
// title: t`Has Project Code`,
// description: t`Show orders with project code`,
// }
// TODO: 'has project code' filter (see table_filters.js)
// TODO: 'project code' filter (see table_filters.js)
];
}, []);
}, [
projectCodeFilters.choices,
userFilters.choices,
responsibleFilters.choices
]);
const user = useUserState();

View File

@@ -116,13 +116,13 @@ export default function BuildOutputTable({
/>,
<ActionButton
tooltip={t`Scrap selected outputs`}
icon={<InvenTreeIcon icon="cancel" />}
icon={<InvenTreeIcon icon="delete" />}
color="red"
disabled={!table.hasSelectedRecords}
/>,
<ActionButton
tooltip={t`Cancel selected outputs`}
icon={<InvenTreeIcon icon="delete" />}
icon={<InvenTreeIcon icon="cancel" />}
color="red"
disabled={!table.hasSelectedRecords}
/>
@@ -153,14 +153,14 @@ export default function BuildOutputTable({
{
title: t`Scrap`,
tooltip: t`Scrap build output`,
color: 'red',
icon: <InvenTreeIcon icon="cancel" />
icon: <InvenTreeIcon icon="delete" />,
color: 'red'
},
{
title: t`Delete`,
tooltip: t`Delete build output`,
color: 'red',
icon: <InvenTreeIcon icon="delete" />
title: t`Cancel`,
tooltip: t`Cancel build output`,
icon: <InvenTreeIcon icon="cancel" />,
color: 'red'
}
];

View File

@@ -1,7 +1,6 @@
import { t } from '@lingui/macro';
import { Group, Text } from '@mantine/core';
import { ReactNode, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { AddItemButton } from '../../components/buttons/AddItemButton';
import { formatPriceRange } from '../../defaults/formatters';
@@ -9,7 +8,6 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { usePartFields } from '../../forms/PartForms';
import { shortenString } from '../../functions/tables';
import { useCreateApiFormModal } from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
@@ -43,13 +41,7 @@ function partTableColumns(): TableColumn[] {
{
accessor: 'category',
sortable: true,
render: function (record: any) {
// TODO: Link to the category detail page
return shortenString({
str: record.category_detail?.pathstring
});
}
render: (record: any) => record.category_detail?.pathstring
},
{
accessor: 'total_in_stock',

View File

@@ -52,7 +52,11 @@ type ThumbProps = {
/**
* Renders a single image thumbnail
*/
function PartThumbComponent({ selected, element, selectImage }: ThumbProps) {
function PartThumbComponent({
selected,
element,
selectImage
}: Readonly<ThumbProps>) {
const { hovered, ref } = useHover();
const hoverColor = 'rgba(127,127,127,0.2)';

View File

@@ -8,6 +8,7 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { usePurchaseOrderFields } from '../../forms/PurchaseOrderForms';
import { useOwnerFilters, useProjectCodeFilters } from '../../hooks/UseFilter';
import { useCreateApiFormModal } from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
@@ -44,6 +45,9 @@ export function PurchaseOrderTable({
const table = useTable('purchase-order');
const user = useUserState();
const projectCodeFilters = useProjectCodeFilters();
const responsibleFilters = useOwnerFilters();
const tableFilters: TableFilter[] = useMemo(() => {
return [
{
@@ -54,11 +58,26 @@ export function PurchaseOrderTable({
},
OutstandingFilter(),
OverdueFilter(),
AssignedToMeFilter()
// TODO: has_project_code
// TODO: project_code
AssignedToMeFilter(),
{
name: 'project_code',
label: t`Project Code`,
description: t`Filter by project code`,
choices: projectCodeFilters.choices
},
{
name: 'has_project_code',
label: t`Has Project Code`,
description: t`Filter by whether the purchase order has a project code`
},
{
name: 'assigned_to',
label: t`Responsible`,
description: t`Filter by responsible owner`,
choices: responsibleFilters.choices
}
];
}, []);
}, [projectCodeFilters.choices, responsibleFilters.choices]);
const tableColumns = useMemo(() => {
return [

View File

@@ -8,6 +8,7 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { useReturnOrderFields } from '../../forms/SalesOrderForms';
import { useOwnerFilters, useProjectCodeFilters } from '../../hooks/UseFilter';
import { useCreateApiFormModal } from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
@@ -35,6 +36,9 @@ export function ReturnOrderTable({ params }: { params?: any }) {
const table = useTable('return-orders');
const user = useUserState();
const projectCodeFilters = useProjectCodeFilters();
const responsibleFilters = useOwnerFilters();
const tableFilters: TableFilter[] = useMemo(() => {
return [
{
@@ -45,9 +49,26 @@ export function ReturnOrderTable({ params }: { params?: any }) {
},
OutstandingFilter(),
OverdueFilter(),
AssignedToMeFilter()
AssignedToMeFilter(),
{
name: 'project_code',
label: t`Project Code`,
description: t`Filter by project code`,
choices: projectCodeFilters.choices
},
{
name: 'has_project_code',
label: t`Has Project Code`,
description: t`Filter by whether the purchase order has a project code`
},
{
name: 'assigned_to',
label: t`Responsible`,
description: t`Filter by responsible owner`,
choices: responsibleFilters.choices
}
];
}, []);
}, [projectCodeFilters.choices, responsibleFilters.choices]);
const tableColumns = useMemo(() => {
return [

View File

@@ -8,6 +8,7 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { useSalesOrderFields } from '../../forms/SalesOrderForms';
import { useOwnerFilters, useProjectCodeFilters } from '../../hooks/UseFilter';
import { useCreateApiFormModal } from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
@@ -41,6 +42,9 @@ export function SalesOrderTable({
const table = useTable('sales-order');
const user = useUserState();
const projectCodeFilters = useProjectCodeFilters();
const responsibleFilters = useOwnerFilters();
const tableFilters: TableFilter[] = useMemo(() => {
return [
{
@@ -51,11 +55,26 @@ export function SalesOrderTable({
},
OutstandingFilter(),
OverdueFilter(),
AssignedToMeFilter()
// TODO: has_project_code
// TODO: project_code
AssignedToMeFilter(),
{
name: 'project_code',
label: t`Project Code`,
description: t`Filter by project code`,
choices: projectCodeFilters.choices
},
{
name: 'has_project_code',
label: t`Has Project Code`,
description: t`Filter by whether the purchase order has a project code`
},
{
name: 'assigned_to',
label: t`Responsible`,
description: t`Filter by responsible owner`,
choices: responsibleFilters.choices
}
];
}, []);
}, [projectCodeFilters.choices, responsibleFilters.choices]);
const salesOrderFields = useSalesOrderFields();

View File

@@ -4,7 +4,7 @@ import { ReactNode, useMemo } from 'react';
import { AddItemButton } from '../../components/buttons/AddItemButton';
import { ActionDropdown } from '../../components/items/ActionDropdown';
import { formatCurrency, renderDate } from '../../defaults/formatters';
import { formatCurrency } from '../../defaults/formatters';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';

View File

@@ -27,10 +27,19 @@ test('PUI - Parts', async ({ page }) => {
await page.getByText('1551ACLR').click();
await page.getByRole('tab', { name: 'Part Details' }).click();
await page.getByRole('tab', { name: 'Parameters' }).click();
// await page.getByRole('tab', { name: 'Stock' }).click();
await page
.getByRole('tab', { name: 'Part Details' })
.locator('xpath=..')
.getByRole('tab', { name: 'Stock', exact: true })
.click();
await page.getByRole('tab', { name: 'Allocations' }).click();
await page.getByRole('tab', { name: 'Used In' }).click();
await page.getByRole('tab', { name: 'Pricing' }).click();
await page.goto(`${baseUrl}/part/category/index/parts`);
await page.getByText('Blue Chair').click();
await page.getByRole('tab', { name: 'Bill of Materials' }).click();
await page.getByRole('tab', { name: 'Build Orders' }).click();
});
test('PUI - Parts - Manufacturer Parts', async ({ page }) => {

View File

@@ -16,6 +16,14 @@ test('PUI - Stock', async ({ page }) => {
await page.getByRole('tab', { name: 'Stock Locations' }).click();
await page.getByRole('tab', { name: 'Stock Items' }).click();
await page.getByRole('tab', { name: 'Location Details' }).click();
await page.goto(`${baseUrl}/stock/item/1194/details`);
await page.getByText('D.123 | Doohickey').waitFor();
await page.getByText('Batch Code: BX-123-2024-2-7').waitFor();
await page.getByRole('tab', { name: 'Stock Tracking' }).click();
await page.getByRole('tab', { name: 'Test Data' }).click();
await page.getByText('395c6d5586e5fb656901d047be27e1f7').waitFor();
await page.getByRole('tab', { name: 'Installed Items' }).click();
});
test('PUI - Build', async ({ page }) => {