Skip to content

Commit

Permalink
[PUI] Part allocations (#8458)
Browse files Browse the repository at this point in the history
* Add new backend filters for BuildLine API

* PUI: Better display of part allocations against build orders

* Add 'order_outstanding' filter to SalesOrderLineItem API

* Add new table showing outstanding SalesOrder allocations against a part

* Update playwright test

* Cleanup

* Bump API version

* Add more table columns

* Tweak UsedInTable

* Another table tweak

* Tweak playwright tests
  • Loading branch information
SchrodingersGat authored Nov 9, 2024
1 parent ad39d3f commit 255a5d0
Show file tree
Hide file tree
Showing 15 changed files with 459 additions and 83 deletions.
6 changes: 5 additions & 1 deletion src/backend/InvenTree/InvenTree/api_version.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
"""InvenTree API version information."""

# InvenTree API version
INVENTREE_API_VERSION = 278
INVENTREE_API_VERSION = 279

"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""


INVENTREE_API_TEXT = """
v279 - 2024-11-09 : https://github.com/inventree/InvenTree/pull/8458
- Adds "order_outstanding" and "part" filters to the BuildLine API endpoint
- Adds "order_outstanding" filter to the SalesOrderLineItem API endpoint
v278 - 2024-11-07 : https://github.com/inventree/InvenTree/pull/8445
- Updates to the SalesOrder API endpoints
- Add "shipment count" information to the SalesOrder API endpoints
Expand Down
33 changes: 33 additions & 0 deletions src/backend/InvenTree/build/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,23 @@ class Meta:
tracked = rest_filters.BooleanFilter(label=_('Tracked'), field_name='bom_item__sub_part__trackable')
testable = rest_filters.BooleanFilter(label=_('Testable'), field_name='bom_item__sub_part__testable')

part = rest_filters.ModelChoiceFilter(
queryset=part.models.Part.objects.all(),
label=_('Part'),
field_name='bom_item__sub_part',
)

order_outstanding = rest_filters.BooleanFilter(
label=_('Order Outstanding'),
method='filter_order_outstanding'
)

def filter_order_outstanding(self, queryset, name, value):
"""Filter by whether the associated BuildOrder is 'outstanding'."""
if str2bool(value):
return queryset.filter(build__status__in=BuildStatusGroups.ACTIVE_CODES)
return queryset.exclude(build__status__in=BuildStatusGroups.ACTIVE_CODES)

allocated = rest_filters.BooleanFilter(label=_('Allocated'), method='filter_allocated')

def filter_allocated(self, queryset, name, value):
Expand All @@ -383,12 +400,28 @@ def filter_available(self, queryset, name, value):
return queryset.exclude(flt)



class BuildLineEndpoint:
"""Mixin class for BuildLine API endpoints."""

queryset = BuildLine.objects.all()
serializer_class = build.serializers.BuildLineSerializer

def get_serializer(self, *args, **kwargs):
"""Return the serializer instance for this endpoint."""

kwargs['context'] = self.get_serializer_context()

try:
params = self.request.query_params

kwargs['part_detail'] = str2bool(params.get('part_detail', True))
kwargs['build_detail'] = str2bool(params.get('build_detail', False))
except AttributeError:
pass

return self.serializer_class(*args, **kwargs)

def get_source_build(self) -> Build:
"""Return the source Build object for the BuildLine queryset.
Expand Down
25 changes: 23 additions & 2 deletions src/backend/InvenTree/build/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1278,8 +1278,6 @@ class Meta:
'pk',
'build',
'bom_item',
'bom_item_detail',
'part_detail',
'quantity',

# Build detail fields
Expand Down Expand Up @@ -1315,6 +1313,11 @@ class Meta:
# Extra fields only for data export
'part_description',
'part_category_name',

# Extra detail (related field) serializers
'bom_item_detail',
'part_detail',
'build_detail',
]

read_only_fields = [
Expand All @@ -1323,6 +1326,19 @@ class Meta:
'allocations',
]

def __init__(self, *args, **kwargs):
"""Determine which extra details fields should be included"""
part_detail = kwargs.pop('part_detail', True)
build_detail = kwargs.pop('build_detail', False)

super().__init__(*args, **kwargs)

if not part_detail:
self.fields.pop('part_detail', None)

if not build_detail:
self.fields.pop('build_detail', None)

# Build info fields
build_reference = serializers.CharField(source='build.reference', label=_('Build Reference'), read_only=True)

Expand Down Expand Up @@ -1362,6 +1378,7 @@ class Meta:
)

part_detail = part_serializers.PartBriefSerializer(source='bom_item.sub_part', many=False, read_only=True, pricing=False)
build_detail = BuildSerializer(source='build', part_detail=False, many=False, read_only=True)

# Annotated (calculated) fields

Expand Down Expand Up @@ -1404,9 +1421,13 @@ def annotate_queryset(queryset, build=None):
"""
queryset = queryset.select_related(
'build',
'build__part',
'build__part__pricing_data',
'bom_item',
'bom_item__part',
'bom_item__part__pricing_data',
'bom_item__sub_part',
'bom_item__sub_part__pricing_data'
)

# Pre-fetch related fields
Expand Down
11 changes: 11 additions & 0 deletions src/backend/InvenTree/order/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,17 @@ def filter_order_complete(self, queryset, name, value):

return queryset.exclude(order__status__in=SalesOrderStatusGroups.COMPLETE)

order_outstanding = rest_filters.BooleanFilter(
label=_('Order Outstanding'), method='filter_order_outstanding'
)

def filter_order_outstanding(self, queryset, name, value):
"""Filter by whether the order is 'outstanding' or not."""
if str2bool(value):
return queryset.filter(order__status__in=SalesOrderStatusGroups.OPEN)

return queryset.exclude(order__status__in=SalesOrderStatusGroups.OPEN)


class SalesOrderLineItemMixin:
"""Mixin class for SalesOrderLineItem endpoints."""
Expand Down
10 changes: 5 additions & 5 deletions src/frontend/src/forms/BuildForms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -497,9 +497,9 @@ export function useAllocateStockToBuildForm({
lineItems,
onFormSuccess
}: {
buildId: number;
buildId?: number;
outputId?: number | null;
build: any;
build?: any;
lineItems: any[];
onFormSuccess: (response: any) => void;
}) {
Expand Down Expand Up @@ -533,8 +533,8 @@ export function useAllocateStockToBuildForm({
}, [lineItems, sourceLocation]);

useEffect(() => {
setSourceLocation(build.take_from);
}, [build.take_from]);
setSourceLocation(build?.take_from);
}, [build?.take_from]);

const sourceLocationField: ApiFormFieldType = useMemo(() => {
return {
Expand All @@ -545,7 +545,7 @@ export function useAllocateStockToBuildForm({
label: t`Source Location`,
description: t`Select the source location for the stock allocation`,
name: 'source_location',
value: build.take_from,
value: build?.take_from,
onValueChange: (value: any) => {
setSourceLocation(value);
}
Expand Down
21 changes: 4 additions & 17 deletions src/frontend/src/pages/part/PartAllocationPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ import { t } from '@lingui/macro';
import { Accordion } from '@mantine/core';

import { StylishText } from '../../components/items/StylishText';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { useUserState } from '../../states/UserState';
import BuildAllocatedStockTable from '../../tables/build/BuildAllocatedStockTable';
import SalesOrderAllocationTable from '../../tables/sales/SalesOrderAllocationTable';
import PartBuildAllocationsTable from '../../tables/part/PartBuildAllocationsTable';
import PartSalesAllocationsTable from '../../tables/part/PartSalesAllocationsTable';

export default function PartAllocationPanel({ part }: { part: any }) {
const user = useUserState();
Expand All @@ -23,14 +22,7 @@ export default function PartAllocationPanel({ part }: { part: any }) {
<StylishText size="lg">{t`Build Order Allocations`}</StylishText>
</Accordion.Control>
<Accordion.Panel>
<BuildAllocatedStockTable
partId={part.pk}
modelField="build"
modelTarget={ModelType.build}
showBuildInfo
showPartInfo
allowEdit
/>
<PartBuildAllocationsTable partId={part.pk} />
</Accordion.Panel>
</Accordion.Item>
)}
Expand All @@ -40,12 +32,7 @@ export default function PartAllocationPanel({ part }: { part: any }) {
<StylishText size="lg">{t`Sales Order Allocations`}</StylishText>
</Accordion.Control>
<Accordion.Panel>
<SalesOrderAllocationTable
partId={part.pk}
modelField="order"
modelTarget={ModelType.salesorder}
showOrderInfo
/>
<PartSalesAllocationsTable partId={part.pk} />
</Accordion.Panel>
</Accordion.Item>
)}
Expand Down
12 changes: 9 additions & 3 deletions src/frontend/src/tables/ColumnRenderers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,10 +163,16 @@ export function LineItemsProgressColumn(): TableColumn {
export function ProjectCodeColumn(props: TableColumnProps): TableColumn {
return {
accessor: 'project_code',
ordering: 'project_code',
sortable: true,
render: (record: any) => (
<ProjectCodeHoverCard projectCode={record.project_code_detail} />
),
title: t`Project Code`,
render: (record: any) => {
let project_code = resolveItem(
record,
props.accessor ?? 'project_code_detail'
);
return <ProjectCodeHoverCard projectCode={project_code} />;
},
...props
};
}
Expand Down
16 changes: 16 additions & 0 deletions src/frontend/src/tables/RowExpansionIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ActionIcon } from '@mantine/core';
import { IconChevronDown, IconChevronRight } from '@tabler/icons-react';

export default function RowExpansionIcon({
enabled,
expanded
}: {
enabled: boolean;
expanded: boolean;
}) {
return (
<ActionIcon size="sm" variant="transparent" disabled={!enabled}>
{expanded ? <IconChevronDown /> : <IconChevronRight />}
</ActionIcon>
);
}
3 changes: 2 additions & 1 deletion src/frontend/src/tables/bom/UsedInTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,13 @@ export function UsedInTable({
},
{
accessor: 'quantity',
switchable: false,
render: (record: any) => {
let quantity = formatDecimal(record.quantity);
let units = record.sub_part_detail?.units;

return (
<Group justify="space-between" grow>
<Group justify="space-between" grow wrap="nowrap">
<Text>{quantity}</Text>
{units && <Text size="xs">{units}</Text>}
</Group>
Expand Down
45 changes: 23 additions & 22 deletions src/frontend/src/tables/build/BuildLineTable.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { t } from '@lingui/macro';
import { ActionIcon, Alert, Group, Paper, Stack, Text } from '@mantine/core';
import { Alert, Group, Paper, Stack, Text } from '@mantine/core';
import {
IconArrowRight,
IconChevronDown,
IconChevronRight,
IconCircleMinus,
IconShoppingCart,
IconTool,
Expand Down Expand Up @@ -43,6 +41,7 @@ import {
RowEditAction,
RowViewAction
} from '../RowActions';
import RowExpansionIcon from '../RowExpansionIcon';
import { TableHoverCard } from '../TableHoverCard';

/**
Expand All @@ -53,16 +52,17 @@ import { TableHoverCard } from '../TableHoverCard';
*
* Note: We expect that the "lineItem" object contains an allocations[] list
*/
function BuildLineSubTable({
export function BuildLineSubTable({
lineItem,
onEditAllocation,
onDeleteAllocation
}: {
lineItem: any;
onEditAllocation: (pk: number) => void;
onDeleteAllocation: (pk: number) => void;
onEditAllocation?: (pk: number) => void;
onDeleteAllocation?: (pk: number) => void;
}) {
const user = useUserState();
const navigate = useNavigate();

const tableColumns: any[] = useMemo(() => {
return [
Expand Down Expand Up @@ -100,16 +100,24 @@ function BuildLineSubTable({
title={t`Actions`}
index={record.pk}
actions={[
RowViewAction({
title: t`View Stock Item`,
modelType: ModelType.stockitem,
modelId: record.stock_item,
navigate: navigate
}),
RowEditAction({
hidden: !user.hasChangeRole(UserRoles.build),
hidden:
!onEditAllocation || !user.hasChangeRole(UserRoles.build),
onClick: () => {
onEditAllocation(record.pk);
onEditAllocation?.(record.pk);
}
}),
RowDeleteAction({
hidden: !user.hasDeleteRole(UserRoles.build),
hidden:
!onDeleteAllocation || !user.hasDeleteRole(UserRoles.build),
onClick: () => {
onDeleteAllocation(record.pk);
onDeleteAllocation?.(record.pk);
}
})
]}
Expand All @@ -131,7 +139,7 @@ function BuildLineSubTable({
pinLastColumn
idAccessor="pk"
columns={tableColumns}
records={lineItem.filteredAllocations}
records={lineItem.filteredAllocations ?? lineItem.allocations}
/>
</Stack>
</Paper>
Expand Down Expand Up @@ -301,17 +309,10 @@ export default function BuildLineTable({

return (
<Group wrap="nowrap">
<ActionIcon
size="sm"
variant="transparent"
disabled={!hasAllocatedItems}
>
{table.isRowExpanded(record.pk) ? (
<IconChevronDown />
) : (
<IconChevronRight />
)}
</ActionIcon>
<RowExpansionIcon
enabled={hasAllocatedItems}
expanded={table.isRowExpanded(record.pk)}
/>
<PartColumn part={record.part_detail} />
</Group>
);
Expand Down
Loading

0 comments on commit 255a5d0

Please sign in to comment.