mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-03 22:55:43 +00:00 
			
		
		
		
	Merge branch 'master' of https://github.com/inventree/InvenTree into pui-maintine-v7
This commit is contained in:
		@@ -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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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)."""
 | 
			
		||||
 
 | 
			
		||||
@@ -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."""
 | 
			
		||||
 
 | 
			
		||||
@@ -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 />} />;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -36,7 +36,7 @@ export function SplitButton({
 | 
			
		||||
  selected,
 | 
			
		||||
  setSelected,
 | 
			
		||||
  loading
 | 
			
		||||
}: SplitButtonProps) {
 | 
			
		||||
}: Readonly<SplitButtonProps>) {
 | 
			
		||||
  const [current, setCurrent] = useState<string>(defaultSelected);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
 
 | 
			
		||||
@@ -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} />
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -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>();
 | 
			
		||||
 
 | 
			
		||||
@@ -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} />
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ export function DocTooltip({
 | 
			
		||||
  detail,
 | 
			
		||||
  link,
 | 
			
		||||
  docchildren
 | 
			
		||||
}: DocTooltipProps) {
 | 
			
		||||
}: Readonly<DocTooltipProps>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <HoverCard
 | 
			
		||||
      shadow="md"
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ export function TitleWithDoc({
 | 
			
		||||
  size,
 | 
			
		||||
  text,
 | 
			
		||||
  detail
 | 
			
		||||
}: DocTitleProps) {
 | 
			
		||||
}: Readonly<DocTitleProps>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <Group>
 | 
			
		||||
      <Title variant={variant} order={order} size={size}>
 | 
			
		||||
 
 | 
			
		||||
@@ -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} />} />
 | 
			
		||||
 
 | 
			
		||||
@@ -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 && (
 | 
			
		||||
 
 | 
			
		||||
@@ -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} />} />
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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}
 | 
			
		||||
 
 | 
			
		||||
@@ -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 ?? {};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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 '-';
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -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) {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										91
									
								
								src/frontend/src/hooks/UseFilter.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								src/frontend/src/hooks/UseFilter.tsx
									
									
									
									
									
										Normal 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
 | 
			
		||||
    })
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
@@ -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',
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
 
 | 
			
		||||
@@ -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}
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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'
 | 
			
		||||
        }
 | 
			
		||||
      ];
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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',
 | 
			
		||||
 
 | 
			
		||||
@@ -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)';
 | 
			
		||||
 
 | 
			
		||||
@@ -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 [
 | 
			
		||||
 
 | 
			
		||||
@@ -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 [
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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';
 | 
			
		||||
 
 | 
			
		||||
@@ -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 }) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -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 }) => {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user