[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:
Oliver 2024-08-10 09:19:32 +10:00 committed by GitHub
parent 3733e8a417
commit 556a3161e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 181 additions and 8 deletions

View File

@ -1,13 +1,16 @@
"""InvenTree API version information."""
# 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."""
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
- Adds "on_order" quantity to SalesOrderLineItem serializer
- Adds "building" quantity to SalesOrderLineItem serializer

View File

@ -231,6 +231,7 @@ class PurchaseOrderSerializer(
'supplier',
'supplier_detail',
'supplier_reference',
'supplier_name',
'total_price',
'order_currency',
])
@ -278,6 +279,10 @@ class PurchaseOrderSerializer(
return queryset
supplier_name = serializers.CharField(
source='supplier.name', read_only=True, label=_('Supplier Name')
)
supplier_detail = CompanyBriefSerializer(
source='supplier', many=False, read_only=True
)

View File

@ -89,6 +89,7 @@ import BuildAllocatedStockTable from '../../tables/build/BuildAllocatedStockTabl
import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
import { AttachmentTable } from '../../tables/general/AttachmentTable';
import { PartParameterTable } from '../../tables/part/PartParameterTable';
import PartPurchaseOrdersTable from '../../tables/part/PartPurchaseOrdersTable';
import PartTestTemplateTable from '../../tables/part/PartTestTemplateTable';
import { PartVariantTable } from '../../tables/part/PartVariantTable';
import { RelatedPartTable } from '../../tables/part/RelatedPartTable';
@ -648,7 +649,7 @@ export default function PartDetail() {
label: t`Purchase Orders`,
icon: <IconShoppingCart />,
hidden: !part.purchaseable,
content: <PlaceholderPanel />
content: <PartPurchaseOrdersTable partId={part.pk} />
},
{
name: 'sales_orders',

View 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
}}
/>
</>
);
}

View File

@ -21,6 +21,7 @@ import {
useDeleteApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import useStatusCodes from '../../hooks/UseStatusCodes';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
@ -273,13 +274,23 @@ export function PurchaseOrderLineItemTable({
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(
(record: any) => {
let received = (record?.received ?? 0) >= (record?.quantity ?? 0);
return [
{
hidden: received,
hidden: received || !orderOpen,
title: t`Receive line item`,
icon: <IconSquareArrowRight />,
color: 'green',
@ -296,7 +307,7 @@ export function PurchaseOrderLineItemTable({
}
}),
RowDuplicateAction({
hidden: !user.hasAddRole(UserRoles.purchase_order),
hidden: !orderOpen || !user.hasAddRole(UserRoles.purchase_order),
onClick: () => {
setInitialData({ ...record });
newLine.open();
@ -311,14 +322,14 @@ export function PurchaseOrderLineItemTable({
})
];
},
[orderId, user]
[orderId, user, orderOpen]
);
// Custom table actions
const tableActions = useMemo(() => {
return [
<ActionButton
hidden={!user.hasAddRole(UserRoles.purchase_order)}
hidden={!orderOpen || !user.hasAddRole(UserRoles.purchase_order)}
tooltip={t`Import Line Items`}
icon={<IconFileArrowLeft />}
onClick={() => importLineItems.open()}
@ -331,16 +342,17 @@ export function PurchaseOrderLineItemTable({
});
newLine.open();
}}
hidden={!user?.hasAddRole(UserRoles.purchase_order)}
hidden={!orderOpen || !user?.hasAddRole(UserRoles.purchase_order)}
/>,
<ActionButton
text={t`Receive items`}
icon={<IconSquareArrowRight />}
onClick={() => receiveLineItems.open()}
disabled={table.selectedRecords.length === 0}
hidden={!orderOpen || !user.hasChangeRole(UserRoles.purchase_order)}
/>
];
}, [orderId, user, table]);
}, [orderId, user, table, orderOpen]);
return (
<>