2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-08-13 15:20:53 +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

@@ -90,7 +90,7 @@ function detect_envs() {
echo "# Using existing config file: ${INVENTREE_CONFIG_FILE}" echo "# Using existing config file: ${INVENTREE_CONFIG_FILE}"
# Install parser # Install parser
pip install -r ${APP_HOME}/.github/requirements.txt -q pip install --require-hashes -r ${APP_HOME}/.github/requirements.txt -q
# Load config # Load config
local CONF=$(cat ${INVENTREE_CONFIG_FILE} | jc --yaml) local CONF=$(cat ${INVENTREE_CONFIG_FILE} | jc --yaml)

View File

@@ -1,11 +1,14 @@
"""InvenTree API version information.""" """InvenTree API version information."""
# InvenTree API version # 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.""" """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 = """
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 v192 - 2024-04-23 : https://github.com/inventree/InvenTree/pull/7106
- Adds 'trackable' ordering option to BuildLineLabel API endpoint - 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.exclude(project_code=None)
return queryset.filter(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): class LineItemFilter(rest_filters.FilterSet):
"""Base class for custom API filters for order line item list(s).""" """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.""" """Abstract serializer class which provides fields common to all order types."""
# Number of line items in this order # 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) # 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) # Human-readable status text (read-only)
status_text = serializers.CharField(source='get_status_display', read_only=True) status_text = serializers.CharField(source='get_status_display', read_only=True)
# status field cannot be set directly # 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 string is *required*
reference = serializers.CharField(required=True) reference = serializers.CharField(required=True)
@@ -114,7 +116,9 @@ class AbstractOrderSerializer(serializers.Serializer):
barcode_hash = serializers.CharField(read_only=True) 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): def validate_reference(self, reference):
"""Custom validation for the reference field.""" """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 * 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 />} />; return <ActionButton {...props} color="green" icon={<IconPlus />} />;
} }

View File

@@ -36,7 +36,7 @@ export function SplitButton({
selected, selected,
setSelected, setSelected,
loading loading
}: SplitButtonProps) { }: Readonly<SplitButtonProps>) {
const [current, setCurrent] = useState<string>(defaultSelected); const [current, setCurrent] = useState<string>(defaultSelected);
useEffect(() => { 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 owner is defined, only renders a badge
* If user is defined, a badge is rendered in addition to main value * 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; let value = props?.field_value;
if (value === undefined) { 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} />; return <YesNoButton value={props.field_value} />;
} }
function TableAnchorValue(props: FieldProps) { function TableAnchorValue(props: Readonly<FieldProps>) {
if (props.field_data.external) { if (props.field_data.external) {
return ( return (
<Anchor <Anchor
@@ -303,7 +303,7 @@ function TableAnchorValue(props: FieldProps) {
); );
} }
function ProgressBarValue(props: FieldProps) { function ProgressBarValue(props: Readonly<FieldProps>) {
return ( return (
<ProgressBar <ProgressBar
value={props.field_data.progress} value={props.field_data.progress}
@@ -313,7 +313,7 @@ function ProgressBarValue(props: FieldProps) {
); );
} }
function StatusValue(props: FieldProps) { function StatusValue(props: Readonly<FieldProps>) {
return ( return (
<StatusRenderer type={props.field_data.model} status={props.field_value} /> <StatusRenderer type={props.field_data.model} status={props.field_value} />
); );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,10 @@
import { ReactNode } from 'react'; 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 ( return (
instance && ( instance && (
<RenderInlineModel <RenderInlineModel

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,11 @@
import { IconUser, IconUsersGroup } from '@tabler/icons-react'; import { IconUser, IconUsersGroup } from '@tabler/icons-react';
import { ReactNode } from '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 ( return (
instance && ( instance && (
<RenderInlineModel <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 ( return (
instance && ( instance && (
<RenderInlineModel <RenderInlineModel

View File

@@ -6,13 +6,13 @@ import {
useUserSettingsState useUserSettingsState
} from '../states/SettingsState'; } from '../states/SettingsState';
interface formatDecmimalOptionsType { interface FormatDecmimalOptionsInterface {
digits?: number; digits?: number;
minDigits?: number; minDigits?: number;
locale?: string; locale?: string;
} }
interface formatCurrencyOptionsType { interface FormatCurrencyOptionsInterface {
digits?: number; digits?: number;
minDigits?: number; minDigits?: number;
currency?: string; currency?: string;
@@ -22,7 +22,7 @@ interface formatCurrencyOptionsType {
export function formatDecimal( export function formatDecimal(
value: number | null | undefined, value: number | null | undefined,
options: formatDecmimalOptionsType = {} options: FormatDecmimalOptionsInterface = {}
) { ) {
let locale = options.locale || navigator.language || 'en-US'; let locale = options.locale || navigator.language || 'en-US';
@@ -45,7 +45,7 @@ export function formatDecimal(
*/ */
export function formatCurrency( export function formatCurrency(
value: number | string | null | undefined, value: number | string | null | undefined,
options: formatCurrencyOptionsType = {} options: FormatCurrencyOptionsInterface = {}
) { ) {
if (value == null || value == undefined) { if (value == null || value == undefined) {
return null; return null;
@@ -90,7 +90,7 @@ export function formatCurrency(
export function formatPriceRange( export function formatPriceRange(
minValue: number | null, minValue: number | null,
maxValue: number | null, maxValue: number | null,
options: formatCurrencyOptionsType = {} options: FormatCurrencyOptionsInterface = {}
) { ) {
// If neither values are provided, return a dash // If neither values are provided, return a dash
if (minValue == null && maxValue == null) { if (minValue == null && maxValue == null) {
@@ -117,7 +117,7 @@ export function formatPriceRange(
)}`; )}`;
} }
interface renderDateOptionsType { interface RenderDateOptionsInterface {
showTime?: boolean; showTime?: boolean;
showSeconds?: 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 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. * 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) { if (!date) {
return '-'; return '-';
} }

View File

@@ -217,7 +217,7 @@ type InvenTreeIconProps = {
iconProps?: TablerIconProps; iconProps?: TablerIconProps;
}; };
export function InvenTreeIcon(props: InvenTreeIconProps) { export function InvenTreeIcon(props: Readonly<InvenTreeIconProps>) {
let Icon: React.ForwardRefExoticComponent<React.RefAttributes<any>>; let Icon: React.ForwardRefExoticComponent<React.RefAttributes<any>>;
if (props.icon in icons) { 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, Badge,
Button, Button,
Checkbox, Checkbox,
Col,
Container, Container,
Grid, Grid,
Group, Group,
@@ -479,11 +480,11 @@ enum InputMethod {
ImageBarcode = 'imageBarcode' ImageBarcode = 'imageBarcode'
} }
interface inputProps { interface ScanInputInterface {
action: (items: ScanItem[]) => void; action: (items: ScanItem[]) => void;
} }
function InputManual({ action }: inputProps) { function InputManual({ action }: Readonly<ScanInputInterface>) {
const [value, setValue] = useState<string>(''); const [value, setValue] = useState<string>('');
function btnAddItem() { function btnAddItem() {
@@ -535,7 +536,7 @@ function InputManual({ action }: inputProps) {
} }
/* Input that uses QR code detection from images */ /* 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 [qrCodeScanner, setQrCodeScanner] = useState<Html5Qrcode | null>(null);
const [camId, setCamId] = useLocalStorage<CameraDevice | null>({ const [camId, setCamId] = useLocalStorage<CameraDevice | null>({
key: 'camId', key: 'camId',

View File

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

View File

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

View File

@@ -10,6 +10,11 @@ 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 {
useOwnerFilters,
useProjectCodeFilters,
useUserFilters
} from '../../hooks/UseFilter';
import { useCreateApiFormModal } from '../../hooks/UseForm'; import { useCreateApiFormModal } from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable'; import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
@@ -92,6 +97,10 @@ export function BuildOrderTable({
}) { }) {
const tableColumns = useMemo(() => buildOrderTableColumns(), []); const tableColumns = useMemo(() => buildOrderTableColumns(), []);
const projectCodeFilters = useProjectCodeFilters();
const userFilters = useUserFilters();
const responsibleFilters = useOwnerFilters();
const tableFilters: TableFilter[] = useMemo(() => { const tableFilters: TableFilter[] = useMemo(() => {
return [ return [
{ {
@@ -117,18 +126,36 @@ export function BuildOrderTable({
type: 'boolean', type: 'boolean',
label: t`Assigned to me`, label: t`Assigned to me`,
description: t`Show orders 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(); const user = useUserState();

View File

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

View File

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

View File

@@ -52,7 +52,11 @@ type ThumbProps = {
/** /**
* Renders a single image thumbnail * Renders a single image thumbnail
*/ */
function PartThumbComponent({ selected, element, selectImage }: ThumbProps) { function PartThumbComponent({
selected,
element,
selectImage
}: Readonly<ThumbProps>) {
const { hovered, ref } = useHover(); const { hovered, ref } = useHover();
const hoverColor = 'rgba(127,127,127,0.2)'; 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 { 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 { useOwnerFilters, useProjectCodeFilters } from '../../hooks/UseFilter';
import { useCreateApiFormModal } from '../../hooks/UseForm'; import { useCreateApiFormModal } from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable'; import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
@@ -44,6 +45,9 @@ export function PurchaseOrderTable({
const table = useTable('purchase-order'); const table = useTable('purchase-order');
const user = useUserState(); const user = useUserState();
const projectCodeFilters = useProjectCodeFilters();
const responsibleFilters = useOwnerFilters();
const tableFilters: TableFilter[] = useMemo(() => { const tableFilters: TableFilter[] = useMemo(() => {
return [ return [
{ {
@@ -54,11 +58,26 @@ export function PurchaseOrderTable({
}, },
OutstandingFilter(), OutstandingFilter(),
OverdueFilter(), OverdueFilter(),
AssignedToMeFilter() AssignedToMeFilter(),
// TODO: has_project_code {
// TODO: project_code 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(() => { const tableColumns = useMemo(() => {
return [ return [

View File

@@ -8,6 +8,7 @@ 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 { useOwnerFilters, useProjectCodeFilters } from '../../hooks/UseFilter';
import { useCreateApiFormModal } from '../../hooks/UseForm'; import { useCreateApiFormModal } from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable'; import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
@@ -35,6 +36,9 @@ export function ReturnOrderTable({ params }: { params?: any }) {
const table = useTable('return-orders'); const table = useTable('return-orders');
const user = useUserState(); const user = useUserState();
const projectCodeFilters = useProjectCodeFilters();
const responsibleFilters = useOwnerFilters();
const tableFilters: TableFilter[] = useMemo(() => { const tableFilters: TableFilter[] = useMemo(() => {
return [ return [
{ {
@@ -45,9 +49,26 @@ export function ReturnOrderTable({ params }: { params?: any }) {
}, },
OutstandingFilter(), OutstandingFilter(),
OverdueFilter(), 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(() => { const tableColumns = useMemo(() => {
return [ return [

View File

@@ -8,6 +8,7 @@ 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 { useOwnerFilters, useProjectCodeFilters } from '../../hooks/UseFilter';
import { useCreateApiFormModal } from '../../hooks/UseForm'; import { useCreateApiFormModal } from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable'; import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
@@ -41,6 +42,9 @@ export function SalesOrderTable({
const table = useTable('sales-order'); const table = useTable('sales-order');
const user = useUserState(); const user = useUserState();
const projectCodeFilters = useProjectCodeFilters();
const responsibleFilters = useOwnerFilters();
const tableFilters: TableFilter[] = useMemo(() => { const tableFilters: TableFilter[] = useMemo(() => {
return [ return [
{ {
@@ -51,11 +55,26 @@ export function SalesOrderTable({
}, },
OutstandingFilter(), OutstandingFilter(),
OverdueFilter(), OverdueFilter(),
AssignedToMeFilter() AssignedToMeFilter(),
// TODO: has_project_code {
// TODO: project_code 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(); const salesOrderFields = useSalesOrderFields();

View File

@@ -4,7 +4,7 @@ import { ReactNode, useMemo } from 'react';
import { AddItemButton } from '../../components/buttons/AddItemButton'; import { AddItemButton } from '../../components/buttons/AddItemButton';
import { ActionDropdown } from '../../components/items/ActionDropdown'; import { ActionDropdown } from '../../components/items/ActionDropdown';
import { formatCurrency, renderDate } from '../../defaults/formatters'; import { formatCurrency } from '../../defaults/formatters';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; 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';

View File

@@ -27,10 +27,19 @@ test('PUI - Parts', async ({ page }) => {
await page.getByText('1551ACLR').click(); await page.getByText('1551ACLR').click();
await page.getByRole('tab', { name: 'Part Details' }).click(); await page.getByRole('tab', { name: 'Part Details' }).click();
await page.getByRole('tab', { name: 'Parameters' }).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: 'Allocations' }).click();
await page.getByRole('tab', { name: 'Used In' }).click(); await page.getByRole('tab', { name: 'Used In' }).click();
await page.getByRole('tab', { name: 'Pricing' }).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 }) => { 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 Locations' }).click();
await page.getByRole('tab', { name: 'Stock Items' }).click(); await page.getByRole('tab', { name: 'Stock Items' }).click();
await page.getByRole('tab', { name: 'Location Details' }).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 }) => { test('PUI - Build', async ({ page }) => {