diff --git a/src/frontend/src/components/images/Thumbnail.tsx b/src/frontend/src/components/images/Thumbnail.tsx
index 2b63b0f16a..63e2414122 100644
--- a/src/frontend/src/components/images/Thumbnail.tsx
+++ b/src/frontend/src/components/images/Thumbnail.tsx
@@ -50,49 +50,3 @@ export function Thumbnail({
);
}
-
-export function ThumbnailHoverCard({
- src,
- text,
- link = '',
- alt = t`Thumbnail`,
- size = 20
-}: {
- src: string;
- text: string;
- link?: string;
- alt?: string;
- size?: number;
-}) {
- const card = useMemo(() => {
- return (
-
-
- {text}
-
- );
- }, [src, text, alt, size]);
-
- if (link) {
- return (
-
- {card}
-
- );
- }
-
- return
{card}
;
-}
-
-export function PartHoverCard({ part }: { part: any }) {
- return part ? (
-
- ) : (
-
- );
-}
diff --git a/src/frontend/src/forms/SalesOrderForms.tsx b/src/frontend/src/forms/SalesOrderForms.tsx
index 9c97f13201..9c11eb5c50 100644
--- a/src/frontend/src/forms/SalesOrderForms.tsx
+++ b/src/frontend/src/forms/SalesOrderForms.tsx
@@ -47,6 +47,43 @@ export function useSalesOrderFields(): ApiFormFieldSet {
}, []);
}
+export function useSalesOrderLineItemFields({
+ customerId,
+ orderId,
+ create
+}: {
+ customerId?: number;
+ orderId?: number;
+ create?: boolean;
+}): ApiFormFieldSet {
+ const fields = useMemo(() => {
+ return {
+ order: {
+ filters: {
+ customer_detail: true
+ },
+ disabled: true,
+ value: create ? orderId : undefined
+ },
+ part: {
+ filters: {
+ active: true,
+ salable: true
+ }
+ },
+ reference: {},
+ quantity: {},
+ sale_price: {},
+ sale_price_currency: {},
+ target_date: {},
+ notes: {},
+ link: {}
+ };
+ }, []);
+
+ return fields;
+}
+
export function useReturnOrderFields(): ApiFormFieldSet {
return useMemo(() => {
return {
diff --git a/src/frontend/src/pages/sales/SalesOrderDetail.tsx b/src/frontend/src/pages/sales/SalesOrderDetail.tsx
index 967749a5c4..5fb6c861c6 100644
--- a/src/frontend/src/pages/sales/SalesOrderDetail.tsx
+++ b/src/frontend/src/pages/sales/SalesOrderDetail.tsx
@@ -47,6 +47,7 @@ import { useInstance } from '../../hooks/UseInstance';
import { useUserState } from '../../states/UserState';
import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
import { AttachmentTable } from '../../tables/general/AttachmentTable';
+import SalesOrderLineItemTable from '../../tables/sales/SalesOrderLineItemTable';
/**
* Detail page for a single SalesOrder
@@ -249,7 +250,12 @@ export default function SalesOrderDetail() {
name: 'line-items',
label: t`Line Items`,
icon: ,
- content:
+ content: (
+
+ )
},
{
name: 'pending-shipments',
diff --git a/src/frontend/src/tables/bom/UsedInTable.tsx b/src/frontend/src/tables/bom/UsedInTable.tsx
index f9227f92e2..bd0e7d8e14 100644
--- a/src/frontend/src/tables/bom/UsedInTable.tsx
+++ b/src/frontend/src/tables/bom/UsedInTable.tsx
@@ -2,14 +2,13 @@ import { t } from '@lingui/macro';
import { Group, Text } from '@mantine/core';
import { useMemo } from 'react';
-import { PartHoverCard } from '../../components/images/Thumbnail';
import { formatDecimal } from '../../defaults/formatters';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
import { TableColumn } from '../Column';
-import { ReferenceColumn } from '../ColumnRenderers';
+import { PartColumn, ReferenceColumn } from '../ColumnRenderers';
import { TableFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
@@ -31,12 +30,14 @@ export function UsedInTable({
accessor: 'part',
switchable: false,
sortable: true,
- render: (record: any) =>
+ title: t`Assembly`,
+ render: (record: any) => PartColumn(record.part_detail)
},
{
accessor: 'sub_part',
sortable: true,
- render: (record: any) =>
+ title: t`Component`,
+ render: (record: any) => PartColumn(record.sub_part_detail)
},
{
accessor: 'quantity',
diff --git a/src/frontend/src/tables/build/BuildLineTable.tsx b/src/frontend/src/tables/build/BuildLineTable.tsx
index 90ee524b51..5d0b6c90ac 100644
--- a/src/frontend/src/tables/build/BuildLineTable.tsx
+++ b/src/frontend/src/tables/build/BuildLineTable.tsx
@@ -7,7 +7,6 @@ import {
} from '@tabler/icons-react';
import { useCallback, useMemo } from 'react';
-import { PartHoverCard } from '../../components/images/Thumbnail';
import { ProgressBar } from '../../components/items/ProgressBar';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
@@ -15,7 +14,7 @@ import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import { TableColumn } from '../Column';
-import { BooleanColumn } from '../ColumnRenderers';
+import { BooleanColumn, PartColumn } from '../ColumnRenderers';
import { TableFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
import { TableHoverCard } from '../TableHoverCard';
@@ -131,7 +130,7 @@ export default function BuildLineTable({ params = {} }: { params?: any }) {
ordering: 'part',
sortable: true,
switchable: false,
- render: (record: any) =>
+ render: (record: any) => PartColumn(record.part_detail)
},
{
accessor: 'bom_item_detail.reference',
diff --git a/src/frontend/src/tables/build/BuildOrderTable.tsx b/src/frontend/src/tables/build/BuildOrderTable.tsx
index 868840f551..18ff6f4f05 100644
--- a/src/frontend/src/tables/build/BuildOrderTable.tsx
+++ b/src/frontend/src/tables/build/BuildOrderTable.tsx
@@ -2,7 +2,6 @@ import { t } from '@lingui/macro';
import { useMemo } from 'react';
import { AddItemButton } from '../../components/buttons/AddItemButton';
-import { PartHoverCard } from '../../components/images/Thumbnail';
import { ProgressBar } from '../../components/items/ProgressBar';
import { RenderUser } from '../../components/render/User';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
@@ -22,6 +21,7 @@ import { TableColumn } from '../Column';
import {
CreationDateColumn,
DateColumn,
+ PartColumn,
ProjectCodeColumn,
ReferenceColumn,
ResponsibleColumn,
@@ -41,7 +41,7 @@ function buildOrderTableColumns(): TableColumn[] {
accessor: 'part',
sortable: true,
switchable: false,
- render: (record: any) =>
+ render: (record: any) => PartColumn(record.part_detail)
},
{
accessor: 'title',
diff --git a/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx b/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx
index 5166469637..5d10c1484c 100644
--- a/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx
+++ b/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx
@@ -5,7 +5,6 @@ import { useCallback, useMemo, useState } from 'react';
import { ActionButton } from '../../components/buttons/ActionButton';
import { AddItemButton } from '../../components/buttons/AddItemButton';
-import { Thumbnail } from '../../components/images/Thumbnail';
import ImporterDrawer from '../../components/importer/ImporterDrawer';
import { ProgressBar } from '../../components/items/ProgressBar';
import { RenderStockLocation } from '../../components/render/Stock';
@@ -30,6 +29,7 @@ import {
CurrencyColumn,
LinkColumn,
NoteColumn,
+ PartColumn,
ReferenceColumn,
TargetDateColumn,
TotalPriceColumn
@@ -124,14 +124,7 @@ export function PurchaseOrderLineItemTable({
title: t`Internal Part`,
sortable: true,
switchable: false,
- render: (record: any) => {
- return (
-
- );
- }
+ render: (record: any) => PartColumn(record.part_detail)
},
{
accessor: 'description',
diff --git a/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx b/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx
new file mode 100644
index 0000000000..957a32cdb2
--- /dev/null
+++ b/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx
@@ -0,0 +1,272 @@
+import { t } from '@lingui/macro';
+import { Text } from '@mantine/core';
+import { IconSquareArrowRight } from '@tabler/icons-react';
+import { ReactNode, useCallback, useMemo, useState } from 'react';
+
+import { AddItemButton } from '../../components/buttons/AddItemButton';
+import { ProgressBar } from '../../components/items/ProgressBar';
+import { formatCurrency } from '../../defaults/formatters';
+import { ApiEndpoints } from '../../enums/ApiEndpoints';
+import { ModelType } from '../../enums/ModelType';
+import { UserRoles } from '../../enums/Roles';
+import { useSalesOrderLineItemFields } from '../../forms/SalesOrderForms';
+import {
+ useCreateApiFormModal,
+ useDeleteApiFormModal,
+ useEditApiFormModal
+} from '../../hooks/UseForm';
+import { useTable } from '../../hooks/UseTable';
+import { apiUrl } from '../../states/ApiState';
+import { useUserState } from '../../states/UserState';
+import { TableColumn } from '../Column';
+import { DateColumn, LinkColumn, PartColumn } from '../ColumnRenderers';
+import { InvenTreeTable } from '../InvenTreeTable';
+import {
+ RowDeleteAction,
+ RowDuplicateAction,
+ RowEditAction
+} from '../RowActions';
+import { TableHoverCard } from '../TableHoverCard';
+
+export default function SalesOrderLineItemTable({
+ orderId,
+ customerId
+}: {
+ orderId: number;
+ customerId: number;
+}) {
+ const user = useUserState();
+ const table = useTable('sales-order-line-item');
+
+ const tableColumns: TableColumn[] = useMemo(() => {
+ return [
+ {
+ accessor: 'part',
+ sortable: true,
+ switchable: false,
+ render: (record: any) => PartColumn(record?.part_detail)
+ },
+ {
+ accessor: 'part_detail.IPN',
+ title: t`IPN`,
+ switchable: true
+ },
+ {
+ accessor: 'part_detail.description',
+ title: t`Description`,
+ sortable: false,
+ switchable: true
+ },
+ {
+ accessor: 'reference',
+ sortable: false,
+ switchable: true
+ },
+ {
+ accessor: 'quantity',
+ sortable: true
+ },
+ {
+ accessor: 'sale_price',
+ render: (record: any) =>
+ formatCurrency(record.sale_price, {
+ currency: record.sale_price_currency
+ })
+ },
+ {
+ accessor: 'total_price',
+ title: t`Total Price`,
+ render: (record: any) =>
+ formatCurrency(record.sale_price, {
+ currency: record.sale_price_currency,
+ multiplier: record.quantity
+ })
+ },
+ DateColumn({
+ accessor: 'target_date',
+ sortable: true,
+ title: t`Target Date`
+ }),
+ {
+ accessor: 'stock',
+ title: t`Available Stock`,
+ render: (record: any) => {
+ let part_stock = record?.available_stock ?? 0;
+ let variant_stock = record?.available_variant_stock ?? 0;
+ let available = part_stock + variant_stock;
+
+ let required = Math.max(
+ record.quantity - record.allocated - record.shipped,
+ 0
+ );
+
+ let color: string | undefined = undefined;
+ let text: string = `${available}`;
+
+ let extra: ReactNode[] = [];
+
+ if (available <= 0) {
+ color = 'red';
+ text = t`No stock available`;
+ } else if (available < required) {
+ color = 'orange';
+ }
+
+ if (variant_stock > 0) {
+ extra.push({t`Includes variant stock`});
+ }
+
+ return (
+ {text}}
+ extra={extra}
+ title={t`Stock Information`}
+ />
+ );
+ }
+ },
+ {
+ accessor: 'allocated',
+ render: (record: any) => (
+
+ )
+ },
+ {
+ accessor: 'shipped',
+ render: (record: any) => (
+
+ )
+ },
+ {
+ accessor: 'notes'
+ },
+ LinkColumn({
+ accessor: 'link'
+ })
+ ];
+ }, []);
+
+ const [selectedLine, setSelectedLine] = useState(0);
+
+ const [initialData, setInitialData] = useState({});
+
+ const createLineFields = useSalesOrderLineItemFields({
+ orderId: orderId,
+ customerId: customerId,
+ create: true
+ });
+
+ const newLine = useCreateApiFormModal({
+ url: ApiEndpoints.sales_order_line_list,
+ title: t`Add Line Item`,
+ fields: createLineFields,
+ initialData: initialData,
+ table: table
+ });
+
+ const editLineFields = useSalesOrderLineItemFields({
+ orderId: orderId,
+ customerId: customerId,
+ create: false
+ });
+
+ const editLine = useEditApiFormModal({
+ url: ApiEndpoints.sales_order_line_list,
+ pk: selectedLine,
+ title: t`Edit Line Item`,
+ fields: editLineFields,
+ table: table
+ });
+
+ const deleteLine = useDeleteApiFormModal({
+ url: ApiEndpoints.sales_order_line_list,
+ pk: selectedLine,
+ title: t`Delete Line Item`,
+ table: table
+ });
+
+ const tableActions = useMemo(() => {
+ return [
+ {
+ setInitialData({
+ order: orderId
+ });
+ newLine.open();
+ }}
+ hidden={!user.hasAddRole(UserRoles.sales_order)}
+ />
+ ];
+ }, [user]);
+
+ const rowActions = useCallback(
+ (record: any) => {
+ const allocated = (record?.allocated ?? 0) > (record?.quantity ?? 0);
+
+ return [
+ {
+ hidden: allocated || !user.hasChangeRole(UserRoles.sales_order),
+ title: t`Allocate stock`,
+ icon: ,
+ color: 'green'
+ },
+ RowEditAction({
+ hidden: !user.hasChangeRole(UserRoles.sales_order),
+ onClick: () => {
+ setSelectedLine(record.pk);
+ editLine.open();
+ }
+ }),
+ RowDuplicateAction({
+ hidden: !user.hasAddRole(UserRoles.sales_order),
+ onClick: () => {
+ setInitialData(record);
+ newLine.open();
+ }
+ }),
+ RowDeleteAction({
+ hidden: !user.hasDeleteRole(UserRoles.sales_order),
+ onClick: () => {
+ setSelectedLine(record.pk);
+ deleteLine.open();
+ }
+ })
+ ];
+ },
+ [user]
+ );
+
+ return (
+ <>
+ {editLine.modal}
+ {deleteLine.modal}
+ {newLine.modal}
+
+ >
+ );
+}