mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
[PUI] Part po table (#7844)
* Implement table for part purchase orders * Add filters * Improve table * Adjust PO actions based on status * Bump API version
This commit is contained in:
parent
3733e8a417
commit
556a3161e8
@ -1,13 +1,16 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 235
|
INVENTREE_API_VERSION = 236
|
||||||
|
|
||||||
"""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 = """
|
||||||
|
|
||||||
|
v236 - 2024-08-10 : https://github.com/inventree/InvenTree/pull/7844
|
||||||
|
- Adds "supplier_name" to the PurchaseOrder API serializer
|
||||||
|
|
||||||
v235 - 2024-08-08 : https://github.com/inventree/InvenTree/pull/7837
|
v235 - 2024-08-08 : https://github.com/inventree/InvenTree/pull/7837
|
||||||
- Adds "on_order" quantity to SalesOrderLineItem serializer
|
- Adds "on_order" quantity to SalesOrderLineItem serializer
|
||||||
- Adds "building" quantity to SalesOrderLineItem serializer
|
- Adds "building" quantity to SalesOrderLineItem serializer
|
||||||
|
@ -231,6 +231,7 @@ class PurchaseOrderSerializer(
|
|||||||
'supplier',
|
'supplier',
|
||||||
'supplier_detail',
|
'supplier_detail',
|
||||||
'supplier_reference',
|
'supplier_reference',
|
||||||
|
'supplier_name',
|
||||||
'total_price',
|
'total_price',
|
||||||
'order_currency',
|
'order_currency',
|
||||||
])
|
])
|
||||||
@ -278,6 +279,10 @@ class PurchaseOrderSerializer(
|
|||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
supplier_name = serializers.CharField(
|
||||||
|
source='supplier.name', read_only=True, label=_('Supplier Name')
|
||||||
|
)
|
||||||
|
|
||||||
supplier_detail = CompanyBriefSerializer(
|
supplier_detail = CompanyBriefSerializer(
|
||||||
source='supplier', many=False, read_only=True
|
source='supplier', many=False, read_only=True
|
||||||
)
|
)
|
||||||
|
@ -89,6 +89,7 @@ import BuildAllocatedStockTable from '../../tables/build/BuildAllocatedStockTabl
|
|||||||
import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
|
import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
|
||||||
import { AttachmentTable } from '../../tables/general/AttachmentTable';
|
import { AttachmentTable } from '../../tables/general/AttachmentTable';
|
||||||
import { PartParameterTable } from '../../tables/part/PartParameterTable';
|
import { PartParameterTable } from '../../tables/part/PartParameterTable';
|
||||||
|
import PartPurchaseOrdersTable from '../../tables/part/PartPurchaseOrdersTable';
|
||||||
import PartTestTemplateTable from '../../tables/part/PartTestTemplateTable';
|
import PartTestTemplateTable from '../../tables/part/PartTestTemplateTable';
|
||||||
import { PartVariantTable } from '../../tables/part/PartVariantTable';
|
import { PartVariantTable } from '../../tables/part/PartVariantTable';
|
||||||
import { RelatedPartTable } from '../../tables/part/RelatedPartTable';
|
import { RelatedPartTable } from '../../tables/part/RelatedPartTable';
|
||||||
@ -648,7 +649,7 @@ export default function PartDetail() {
|
|||||||
label: t`Purchase Orders`,
|
label: t`Purchase Orders`,
|
||||||
icon: <IconShoppingCart />,
|
icon: <IconShoppingCart />,
|
||||||
hidden: !part.purchaseable,
|
hidden: !part.purchaseable,
|
||||||
content: <PlaceholderPanel />
|
content: <PartPurchaseOrdersTable partId={part.pk} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'sales_orders',
|
name: 'sales_orders',
|
||||||
|
152
src/frontend/src/tables/part/PartPurchaseOrdersTable.tsx
Normal file
152
src/frontend/src/tables/part/PartPurchaseOrdersTable.tsx
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { Text } from '@mantine/core';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { ProgressBar } from '../../components/items/ProgressBar';
|
||||||
|
import { formatCurrency } from '../../defaults/formatters';
|
||||||
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
|
import { ModelType } from '../../enums/ModelType';
|
||||||
|
import { useTable } from '../../hooks/UseTable';
|
||||||
|
import { apiUrl } from '../../states/ApiState';
|
||||||
|
import { useUserState } from '../../states/UserState';
|
||||||
|
import { TableColumn } from '../Column';
|
||||||
|
import { DateColumn, ReferenceColumn, StatusColumn } from '../ColumnRenderers';
|
||||||
|
import { StatusFilterOptions, TableFilter } from '../Filter';
|
||||||
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
|
import { TableHoverCard } from '../TableHoverCard';
|
||||||
|
|
||||||
|
export default function PartPurchaseOrdersTable({
|
||||||
|
partId
|
||||||
|
}: {
|
||||||
|
partId: number;
|
||||||
|
}) {
|
||||||
|
const table = useTable('partpurchaseorders');
|
||||||
|
const user = useUserState();
|
||||||
|
|
||||||
|
const tableColumns: TableColumn[] = useMemo(() => {
|
||||||
|
return [
|
||||||
|
ReferenceColumn({
|
||||||
|
accessor: 'order_detail.reference',
|
||||||
|
sortable: true,
|
||||||
|
switchable: false,
|
||||||
|
title: t`Purchase Order`
|
||||||
|
}),
|
||||||
|
StatusColumn({
|
||||||
|
accessor: 'order_detail.status',
|
||||||
|
sortable: true,
|
||||||
|
title: t`Status`,
|
||||||
|
model: ModelType.purchaseorder
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
accessor: 'order_detail.supplier_name',
|
||||||
|
title: t`Supplier`,
|
||||||
|
sortable: false,
|
||||||
|
switchable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'supplier_part_detail.SKU',
|
||||||
|
ordering: 'sku',
|
||||||
|
title: t`Supplier Part`,
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'supplier_part_detail.manufacturer_part_detail.MPN',
|
||||||
|
ordering: 'mpn',
|
||||||
|
title: t`Manufacturer Part`,
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'quantity',
|
||||||
|
switchable: false,
|
||||||
|
render: (record: any) => {
|
||||||
|
let supplier_part = record?.supplier_part_detail ?? {};
|
||||||
|
let part = record?.part_detail ?? supplier_part?.part_detail ?? {};
|
||||||
|
let extra = [];
|
||||||
|
|
||||||
|
if (supplier_part.pack_quantity_native != 1) {
|
||||||
|
let total = record.quantity * supplier_part.pack_quantity_native;
|
||||||
|
|
||||||
|
extra.push(
|
||||||
|
<Text key="pack-quantity">
|
||||||
|
{t`Pack Quantity`}: {supplier_part.pack_quantity}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
|
||||||
|
extra.push(
|
||||||
|
<Text key="total-quantity">
|
||||||
|
{t`Total Quantity`}: {total} {part?.units}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableHoverCard
|
||||||
|
value={
|
||||||
|
<ProgressBar
|
||||||
|
value={record.received}
|
||||||
|
maximum={record.quantity}
|
||||||
|
progressLabel
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
extra={extra}
|
||||||
|
title={t`Quantity`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
DateColumn({
|
||||||
|
accessor: 'target_date',
|
||||||
|
title: t`Target Date`
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
accessor: 'purchase_price',
|
||||||
|
render: (record: any) =>
|
||||||
|
formatCurrency(record.purchase_price, {
|
||||||
|
currency: record.purchase_price_currency
|
||||||
|
})
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const tableFilters: TableFilter[] = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'pending',
|
||||||
|
label: t`Pending`,
|
||||||
|
description: t`Show pending orders`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'received',
|
||||||
|
label: t`Received`,
|
||||||
|
description: t`Show received items`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'order_status',
|
||||||
|
label: t`Order Status`,
|
||||||
|
description: t`Filter by order status`,
|
||||||
|
choiceFunction: StatusFilterOptions(ModelType.purchaseorder)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<InvenTreeTable
|
||||||
|
url={apiUrl(ApiEndpoints.purchase_order_line_list)}
|
||||||
|
tableState={table}
|
||||||
|
columns={tableColumns}
|
||||||
|
props={{
|
||||||
|
params: {
|
||||||
|
base_part: partId,
|
||||||
|
part_detail: true,
|
||||||
|
order_detail: true,
|
||||||
|
supplier_detail: true
|
||||||
|
},
|
||||||
|
modelField: 'order',
|
||||||
|
modelType: ModelType.purchaseorder,
|
||||||
|
tableFilters: tableFilters
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -21,6 +21,7 @@ import {
|
|||||||
useDeleteApiFormModal,
|
useDeleteApiFormModal,
|
||||||
useEditApiFormModal
|
useEditApiFormModal
|
||||||
} from '../../hooks/UseForm';
|
} from '../../hooks/UseForm';
|
||||||
|
import useStatusCodes from '../../hooks/UseStatusCodes';
|
||||||
import { useTable } from '../../hooks/UseTable';
|
import { useTable } from '../../hooks/UseTable';
|
||||||
import { apiUrl } from '../../states/ApiState';
|
import { apiUrl } from '../../states/ApiState';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
@ -273,13 +274,23 @@ export function PurchaseOrderLineItemTable({
|
|||||||
table: table
|
table: table
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const poStatus = useStatusCodes({ modelType: ModelType.purchaseorder });
|
||||||
|
|
||||||
|
const orderOpen: boolean = useMemo(() => {
|
||||||
|
return (
|
||||||
|
order.status == poStatus.PENDING ||
|
||||||
|
order.status == poStatus.PLACED ||
|
||||||
|
order.status == poStatus.ON_HOLD
|
||||||
|
);
|
||||||
|
}, [order, poStatus]);
|
||||||
|
|
||||||
const rowActions = useCallback(
|
const rowActions = useCallback(
|
||||||
(record: any) => {
|
(record: any) => {
|
||||||
let received = (record?.received ?? 0) >= (record?.quantity ?? 0);
|
let received = (record?.received ?? 0) >= (record?.quantity ?? 0);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
hidden: received,
|
hidden: received || !orderOpen,
|
||||||
title: t`Receive line item`,
|
title: t`Receive line item`,
|
||||||
icon: <IconSquareArrowRight />,
|
icon: <IconSquareArrowRight />,
|
||||||
color: 'green',
|
color: 'green',
|
||||||
@ -296,7 +307,7 @@ export function PurchaseOrderLineItemTable({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
RowDuplicateAction({
|
RowDuplicateAction({
|
||||||
hidden: !user.hasAddRole(UserRoles.purchase_order),
|
hidden: !orderOpen || !user.hasAddRole(UserRoles.purchase_order),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setInitialData({ ...record });
|
setInitialData({ ...record });
|
||||||
newLine.open();
|
newLine.open();
|
||||||
@ -311,14 +322,14 @@ export function PurchaseOrderLineItemTable({
|
|||||||
})
|
})
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
[orderId, user]
|
[orderId, user, orderOpen]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Custom table actions
|
// Custom table actions
|
||||||
const tableActions = useMemo(() => {
|
const tableActions = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
<ActionButton
|
<ActionButton
|
||||||
hidden={!user.hasAddRole(UserRoles.purchase_order)}
|
hidden={!orderOpen || !user.hasAddRole(UserRoles.purchase_order)}
|
||||||
tooltip={t`Import Line Items`}
|
tooltip={t`Import Line Items`}
|
||||||
icon={<IconFileArrowLeft />}
|
icon={<IconFileArrowLeft />}
|
||||||
onClick={() => importLineItems.open()}
|
onClick={() => importLineItems.open()}
|
||||||
@ -331,16 +342,17 @@ export function PurchaseOrderLineItemTable({
|
|||||||
});
|
});
|
||||||
newLine.open();
|
newLine.open();
|
||||||
}}
|
}}
|
||||||
hidden={!user?.hasAddRole(UserRoles.purchase_order)}
|
hidden={!orderOpen || !user?.hasAddRole(UserRoles.purchase_order)}
|
||||||
/>,
|
/>,
|
||||||
<ActionButton
|
<ActionButton
|
||||||
text={t`Receive items`}
|
text={t`Receive items`}
|
||||||
icon={<IconSquareArrowRight />}
|
icon={<IconSquareArrowRight />}
|
||||||
onClick={() => receiveLineItems.open()}
|
onClick={() => receiveLineItems.open()}
|
||||||
disabled={table.selectedRecords.length === 0}
|
disabled={table.selectedRecords.length === 0}
|
||||||
|
hidden={!orderOpen || !user.hasChangeRole(UserRoles.purchase_order)}
|
||||||
/>
|
/>
|
||||||
];
|
];
|
||||||
}, [orderId, user, table]);
|
}, [orderId, user, table, orderOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
Loading…
Reference in New Issue
Block a user