PUI tweaks (#7144)

* Default progress bars a bit thicker

* Implement useFilters hook

- Adds "project code" filter for order tables

* Add "responsible" filters to backend

* Add more filters to tables

* Bump API version

* Typo fix

* Tweak PartTable

* Tweaks

* remove unused imports
This commit is contained in:
Oliver 2024-04-30 16:21:38 +10:00 committed by GitHub
parent 1ef9512f18
commit a9b932cc32
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 226 additions and 45 deletions

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ export type ProgressBarProps = {
maximum?: number;
label?: string;
progressLabel?: boolean;
size?: string;
};
/**
@ -31,8 +32,8 @@ export function ProgressBar(props: ProgressBarProps) {
<Progress
value={progress}
color={progress < 100 ? 'orange' : progress > 100 ? 'blue' : 'green'}
size="sm"
radius="xs"
size={props.size ?? 'md'}
radius="sm"
/>
</Stack>
);

View File

@ -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

@ -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 [
{
@ -115,18 +124,36 @@ export function BuildOrderTable({
type: 'boolean',
label: t`Assigned to me`,
description: t`Show orders assigned to me`
},
{
name: 'project_code',
label: t`Project Code`,
description: t`Filter by project code`,
choices: projectCodeFilters.choices
},
{
name: 'has_project_code',
label: t`Has Project Code`,
description: t`Filter by whether the purchase order has a project code`
},
{
name: 'issued_by',
label: t`Issued By`,
description: t`Filter by user who issued this order`,
choices: userFilters.choices
},
{
name: 'assigned_to',
label: t`Responsible`,
description: t`Filter by responsible owner`,
choices: responsibleFilters.choices
}
// TODO: 'assigned to' filter
// TODO: 'issued by' filter
// {
// name: 'has_project_code',
// title: t`Has Project Code`,
// description: t`Show orders with project code`,
// }
// TODO: 'has project code' filter (see table_filters.js)
// TODO: 'project code' filter (see table_filters.js)
];
}, []);
}, [
projectCodeFilters.choices,
userFilters.choices,
responsibleFilters.choices
]);
const user = useUserState();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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