2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-02 19:50:59 +00:00

[UI] Add "can build" part info (#9798)

* Add proper serializer to PartRequirements API endpoint

* Add API endpoint

* Display "can_build" quantity

* Add simple playwright tests

* Bump API version

* Updated docs

* Fix formatting

* Consolidate field names

- Match field names to the PartSerializer

* Adjust frontend

* Add "can_build" to BuildDetail page

* Tweak BuildDetail

* Hide until load

* serializer fixes
This commit is contained in:
Oliver
2025-06-18 10:17:39 +10:00
committed by GitHub
parent fe4038205f
commit 62aef238f0
8 changed files with 190 additions and 49 deletions

View File

@ -111,6 +111,7 @@ export enum ApiEndpoints {
part_parameter_template_list = 'part/parameter/template/',
part_thumbs_list = 'part/thumbs/',
part_pricing = 'part/:id/pricing/',
part_requirements = 'part/:id/requirements/',
part_serial_numbers = 'part/:id/serial-numbers/',
part_scheduling = 'part/:id/scheduling/',
part_pricing_internal = 'part/internal-price/',

View File

@ -86,11 +86,24 @@ export default function BuildDetail() {
refetchOnMount: true
});
const { instance: partRequirements, instanceQuery: partRequirementsQuery } =
useInstance({
endpoint: ApiEndpoints.part_requirements,
pk: build?.part,
hasPrimaryKey: true,
defaultValue: {}
});
const detailsPanel = useMemo(() => {
if (instanceQuery.isFetching) {
return <Skeleton />;
}
const data = {
...build,
can_build: partRequirements?.can_build ?? 0
};
const tl: DetailsField[] = [
{
type: 'link',
@ -173,10 +186,17 @@ export default function BuildDetail() {
const tr: DetailsField[] = [
{
type: 'text',
type: 'number',
name: 'quantity',
label: t`Build Quantity`
},
{
type: 'number',
name: 'can_build',
unit: build.part_detail?.units,
label: t`Can Build`,
hidden: partRequirementsQuery.isFetching
},
{
type: 'progressbar',
name: 'completed',
@ -290,15 +310,20 @@ export default function BuildDetail() {
pk={build.part}
/>
<Grid.Col span={{ base: 12, sm: 8 }}>
<DetailsTable fields={tl} item={build} />
<DetailsTable fields={tl} item={data} />
</Grid.Col>
</Grid>
<DetailsTable fields={tr} item={build} />
<DetailsTable fields={bl} item={build} />
<DetailsTable fields={br} item={build} />
<DetailsTable fields={tr} item={data} />
<DetailsTable fields={bl} item={data} />
<DetailsTable fields={br} item={data} />
</ItemDetailsGrid>
);
}, [build, instanceQuery]);
}, [
build,
instanceQuery,
partRequirements,
partRequirementsQuery.isFetching
]);
const buildPanels: PanelType[] = useMemo(() => {
return [

View File

@ -143,6 +143,14 @@ export default function PartDetail() {
refetchOnMount: true
});
const { instance: partRequirements, instanceQuery: partRequirementsQuery } =
useInstance({
endpoint: ApiEndpoints.part_requirements,
pk: id,
hasPrimaryKey: true,
refetchOnMount: true
});
const detailsPanel = useMemo(() => {
if (instanceQuery.isFetching) {
return <Skeleton />;
@ -151,8 +159,23 @@ export default function PartDetail() {
const data = { ...part };
data.required =
(data?.required_for_build_orders ?? 0) +
(data?.required_for_sales_orders ?? 0);
(partRequirements?.required_for_build_orders ??
part?.required_for_build_orders ??
0) +
(partRequirements?.required_for_sales_orders ??
part?.required_for_sales_orders ??
0);
data.allocated =
(partRequirements?.allocated_to_build_orders ??
part?.allocated_to_build_orders ??
0) +
(partRequirements?.allocated_to_sales_orders ??
part?.allocated_to_sales_orders ??
0);
// Extract requirements data
data.can_build = partRequirements?.can_build ?? 0;
// Provide latest serial number info
if (!!serials.latest) {
@ -315,13 +338,6 @@ export default function PartDetail() {
(part.required_for_sales_orders <= 0 &&
part.allocated_to_sales_orders <= 0)
},
{
type: 'number',
name: 'can_build',
unit: true,
label: t`Can Build`,
hidden: true // TODO: Expose "can_build" to the API
},
{
type: 'progressbar',
name: 'building',
@ -329,6 +345,13 @@ export default function PartDetail() {
progress: part.building,
total: part.scheduled_to_build,
hidden: !part.assembly || (!part.building && !part.scheduled_to_build)
},
{
type: 'number',
name: 'can_build',
unit: part.units,
label: t`Can Build`,
hidden: !part.assembly || partRequirementsQuery.isFetching
}
];
@ -489,7 +512,9 @@ export default function PartDetail() {
id,
serials,
instanceQuery.isFetching,
instanceQuery.data
instanceQuery.data,
partRequirementsQuery.isFetching,
partRequirements
]);
// Part data panels (recalculate when part data changes)

View File

@ -162,6 +162,26 @@ test('Parts - Locking', async ({ browser }) => {
await page.getByText('Part parameters cannot be').waitFor();
});
test('Parts - Details', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'part/113/details' });
// Check for expected values on this page
await page.getByText('Required for Orders').waitFor();
await page.getByText('Allocated to Sales Orders').waitFor();
await page.getByText('Can Build').waitFor();
await page.getByText('0 / 10').waitFor();
await page.getByText('4 / 49').waitFor();
// Badges
await page.getByText('Required: 10').waitFor();
await page.getByText('No Stock').waitFor();
await page.getByText('In Production: 4').waitFor();
await page.getByText('Creation Date').waitFor();
await page.getByText('2022-04-29').waitFor();
});
test('Parts - Allocations', async ({ browser }) => {
// Let's look at the allocations for a single stock item
const page = await doCachedLogin(browser, { url: 'stock/item/324/' });