diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py
index a0f0d02acd..0f0a6a6bde 100644
--- a/src/backend/InvenTree/InvenTree/api_version.py
+++ b/src/backend/InvenTree/InvenTree/api_version.py
@@ -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
diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py
index f24b009e38..f94ed0c293 100644
--- a/src/backend/InvenTree/order/serializers.py
+++ b/src/backend/InvenTree/order/serializers.py
@@ -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
)
diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx
index 821766af5f..638f156e5d 100644
--- a/src/frontend/src/pages/part/PartDetail.tsx
+++ b/src/frontend/src/pages/part/PartDetail.tsx
@@ -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: ,
hidden: !part.purchaseable,
- content:
+ content:
},
{
name: 'sales_orders',
diff --git a/src/frontend/src/tables/part/PartPurchaseOrdersTable.tsx b/src/frontend/src/tables/part/PartPurchaseOrdersTable.tsx
new file mode 100644
index 0000000000..4857f75dcf
--- /dev/null
+++ b/src/frontend/src/tables/part/PartPurchaseOrdersTable.tsx
@@ -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(
+
+ {t`Pack Quantity`}: {supplier_part.pack_quantity}
+
+ );
+
+ extra.push(
+
+ {t`Total Quantity`}: {total} {part?.units}
+
+ );
+ }
+
+ return (
+
+ }
+ 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 (
+ <>
+
+ >
+ );
+}
diff --git a/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx b/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx
index 5d10c1484c..895a40beff 100644
--- a/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx
+++ b/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx
@@ -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: ,
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 [
}
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)}
/>,
}
onClick={() => receiveLineItems.open()}
disabled={table.selectedRecords.length === 0}
+ hidden={!orderOpen || !user.hasChangeRole(UserRoles.purchase_order)}
/>
];
- }, [orderId, user, table]);
+ }, [orderId, user, table, orderOpen]);
return (
<>