diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py
index f4ca3b8293..a0f0d02acd 100644
--- a/src/backend/InvenTree/InvenTree/api_version.py
+++ b/src/backend/InvenTree/InvenTree/api_version.py
@@ -1,13 +1,17 @@
"""InvenTree API version information."""
# InvenTree API version
-INVENTREE_API_VERSION = 234
+INVENTREE_API_VERSION = 235
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
+v235 - 2024-08-08 : https://github.com/inventree/InvenTree/pull/7837
+ - Adds "on_order" quantity to SalesOrderLineItem serializer
+ - Adds "building" quantity to SalesOrderLineItem serializer
+
v234 - 2024-08-08 : https://github.com/inventree/InvenTree/pull/7829
- Fixes bug in the plugin metadata endpoint
diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py
index 79e7d9043e..f24b009e38 100644
--- a/src/backend/InvenTree/order/serializers.py
+++ b/src/backend/InvenTree/order/serializers.py
@@ -21,7 +21,6 @@ from rest_framework.serializers import ValidationError
from sql_util.utils import SubqueryCount
import order.models
-import part.filters
import part.filters as part_filters
import part.models as part_models
import stock.models
@@ -1030,8 +1029,6 @@ class SalesOrderLineItemSerializer(
'pk',
'allocated',
'allocations',
- 'available_stock',
- 'available_variant_stock',
'customer_detail',
'quantity',
'reference',
@@ -1046,6 +1043,11 @@ class SalesOrderLineItemSerializer(
'shipped',
'target_date',
'link',
+ # Annotated fields for part stocking information
+ 'available_stock',
+ 'available_variant_stock',
+ 'building',
+ 'on_order',
]
def __init__(self, *args, **kwargs):
@@ -1078,6 +1080,8 @@ class SalesOrderLineItemSerializer(
- "overdue" status (boolean field)
- "available_quantity"
+ - "building"
+ - "on_order"
"""
queryset = queryset.annotate(
overdue=Case(
@@ -1093,11 +1097,11 @@ class SalesOrderLineItemSerializer(
# Annotate each line with the available stock quantity
# To do this, we need to look at the total stock and any allocations
queryset = queryset.alias(
- total_stock=part.filters.annotate_total_stock(reference='part__'),
- allocated_to_sales_orders=part.filters.annotate_sales_order_allocations(
+ total_stock=part_filters.annotate_total_stock(reference='part__'),
+ allocated_to_sales_orders=part_filters.annotate_sales_order_allocations(
reference='part__'
),
- allocated_to_build_orders=part.filters.annotate_build_order_allocations(
+ allocated_to_build_orders=part_filters.annotate_build_order_allocations(
reference='part__'
),
)
@@ -1112,19 +1116,19 @@ class SalesOrderLineItemSerializer(
)
# Filter for "variant" stock: Variant stock items must be salable and active
- variant_stock_query = part.filters.variant_stock_query(
+ variant_stock_query = part_filters.variant_stock_query(
reference='part__'
).filter(part__salable=True, part__active=True)
# Also add in available "variant" stock
queryset = queryset.alias(
- variant_stock_total=part.filters.annotate_variant_quantity(
+ variant_stock_total=part_filters.annotate_variant_quantity(
variant_stock_query, reference='quantity'
),
- variant_bo_allocations=part.filters.annotate_variant_quantity(
+ variant_bo_allocations=part_filters.annotate_variant_quantity(
variant_stock_query, reference='sales_order_allocations__quantity'
),
- variant_so_allocations=part.filters.annotate_variant_quantity(
+ variant_so_allocations=part_filters.annotate_variant_quantity(
variant_stock_query, reference='allocations__quantity'
),
)
@@ -1138,6 +1142,16 @@ class SalesOrderLineItemSerializer(
)
)
+ # Add information about the quantity of parts currently on order
+ queryset = queryset.annotate(
+ on_order=part_filters.annotate_on_order_quantity(reference='part__')
+ )
+
+ # Add information about the quantity of parts currently in production
+ queryset = queryset.annotate(
+ building=part_filters.annotate_in_production_quantity(reference='part__')
+ )
+
return queryset
customer_detail = CompanyBriefSerializer(
@@ -1153,6 +1167,8 @@ class SalesOrderLineItemSerializer(
overdue = serializers.BooleanField(required=False, read_only=True)
available_stock = serializers.FloatField(read_only=True)
available_variant_stock = serializers.FloatField(read_only=True)
+ on_order = serializers.FloatField(label=_('On Order'), read_only=True)
+ building = serializers.FloatField(label=_('In Production'), read_only=True)
quantity = InvenTreeDecimalField()
diff --git a/src/backend/InvenTree/part/serializers.py b/src/backend/InvenTree/part/serializers.py
index 0758a8ff8d..66307ea89d 100644
--- a/src/backend/InvenTree/part/serializers.py
+++ b/src/backend/InvenTree/part/serializers.py
@@ -27,7 +27,7 @@ import company.models
import InvenTree.helpers
import InvenTree.serializers
import InvenTree.status
-import part.filters
+import part.filters as part_filters
import part.helpers as part_helpers
import part.stocktake
import part.tasks
@@ -107,12 +107,12 @@ class CategorySerializer(
"""Annotate extra information to the queryset."""
# Annotate the number of 'parts' which exist in each category (including subcategories!)
queryset = queryset.annotate(
- part_count=part.filters.annotate_category_parts(),
- subcategories=part.filters.annotate_sub_categories(),
+ part_count=part_filters.annotate_category_parts(),
+ subcategories=part_filters.annotate_sub_categories(),
)
queryset = queryset.annotate(
- parent_default_location=part.filters.annotate_default_location('parent__')
+ parent_default_location=part_filters.annotate_default_location('parent__')
)
return queryset
@@ -164,7 +164,7 @@ class CategoryTree(InvenTree.serializers.InvenTreeModelSerializer):
@staticmethod
def annotate_queryset(queryset):
"""Annotate the queryset with the number of subcategories."""
- return queryset.annotate(subcategories=part.filters.annotate_sub_categories())
+ return queryset.annotate(subcategories=part_filters.annotate_sub_categories())
@register_importer()
@@ -781,10 +781,10 @@ class PartSerializer(
queryset = queryset.annotate(stock_item_count=SubqueryCount('stock_items'))
# Annotate with the total variant stock quantity
- variant_query = part.filters.variant_stock_query()
+ variant_query = part_filters.variant_stock_query()
queryset = queryset.annotate(
- variant_stock=part.filters.annotate_variant_quantity(
+ variant_stock=part_filters.annotate_variant_quantity(
variant_query, reference='quantity'
)
)
@@ -814,10 +814,10 @@ class PartSerializer(
# TODO: Note that BomItemSerializer and BuildLineSerializer have very similar code
queryset = queryset.annotate(
- ordering=part.filters.annotate_on_order_quantity(),
- in_stock=part.filters.annotate_total_stock(),
- allocated_to_sales_orders=part.filters.annotate_sales_order_allocations(),
- allocated_to_build_orders=part.filters.annotate_build_order_allocations(),
+ ordering=part_filters.annotate_on_order_quantity(),
+ in_stock=part_filters.annotate_total_stock(),
+ allocated_to_sales_orders=part_filters.annotate_sales_order_allocations(),
+ allocated_to_build_orders=part_filters.annotate_build_order_allocations(),
)
# Annotate the queryset with the 'total_in_stock' quantity
@@ -829,7 +829,7 @@ class PartSerializer(
)
queryset = queryset.annotate(
- external_stock=part.filters.annotate_total_stock(
+ external_stock=part_filters.annotate_total_stock(
filter=Q(location__external=True)
)
)
@@ -847,12 +847,12 @@ class PartSerializer(
# Annotate with the total 'required for builds' quantity
queryset = queryset.annotate(
- required_for_build_orders=part.filters.annotate_build_order_requirements(),
- required_for_sales_orders=part.filters.annotate_sales_order_requirements(),
+ required_for_build_orders=part_filters.annotate_build_order_requirements(),
+ required_for_sales_orders=part_filters.annotate_sales_order_requirements(),
)
queryset = queryset.annotate(
- category_default_location=part.filters.annotate_default_location(
+ category_default_location=part_filters.annotate_default_location(
'category__'
)
)
@@ -1684,30 +1684,23 @@ class BomItemSerializer(
# Annotate with the total "on order" amount for the sub-part
queryset = queryset.annotate(
- on_order=part.filters.annotate_on_order_quantity(ref)
+ on_order=part_filters.annotate_on_order_quantity(ref)
)
# Annotate with the total "building" amount for the sub-part
queryset = queryset.annotate(
- building=Coalesce(
- SubquerySum(
- 'sub_part__builds__quantity',
- filter=Q(status__in=BuildStatusGroups.ACTIVE_CODES),
- ),
- Decimal(0),
- output_field=models.DecimalField(),
- )
+ building=part_filters.annotate_in_production_quantity(ref)
)
# Calculate "total stock" for the referenced sub_part
# Calculate the "build_order_allocations" for the sub_part
# Note that these fields are only aliased, not annotated
queryset = queryset.alias(
- total_stock=part.filters.annotate_total_stock(reference=ref),
- allocated_to_sales_orders=part.filters.annotate_sales_order_allocations(
+ total_stock=part_filters.annotate_total_stock(reference=ref),
+ allocated_to_sales_orders=part_filters.annotate_sales_order_allocations(
reference=ref
),
- allocated_to_build_orders=part.filters.annotate_build_order_allocations(
+ allocated_to_build_orders=part_filters.annotate_build_order_allocations(
reference=ref
),
)
@@ -1724,7 +1717,7 @@ class BomItemSerializer(
# Calculate 'external_stock'
queryset = queryset.annotate(
- external_stock=part.filters.annotate_total_stock(
+ external_stock=part_filters.annotate_total_stock(
reference=ref, filter=Q(location__external=True)
)
)
@@ -1733,11 +1726,11 @@ class BomItemSerializer(
# Extract similar information for any 'substitute' parts
queryset = queryset.alias(
- substitute_stock=part.filters.annotate_total_stock(reference=ref),
- substitute_build_allocations=part.filters.annotate_build_order_allocations(
+ substitute_stock=part_filters.annotate_total_stock(reference=ref),
+ substitute_build_allocations=part_filters.annotate_build_order_allocations(
reference=ref
),
- substitute_sales_allocations=part.filters.annotate_sales_order_allocations(
+ substitute_sales_allocations=part_filters.annotate_sales_order_allocations(
reference=ref
),
)
@@ -1753,16 +1746,16 @@ class BomItemSerializer(
)
# Annotate the queryset with 'available variant stock' information
- variant_stock_query = part.filters.variant_stock_query(reference='sub_part__')
+ variant_stock_query = part_filters.variant_stock_query(reference='sub_part__')
queryset = queryset.alias(
- variant_stock_total=part.filters.annotate_variant_quantity(
+ variant_stock_total=part_filters.annotate_variant_quantity(
variant_stock_query, reference='quantity'
),
- variant_bo_allocations=part.filters.annotate_variant_quantity(
+ variant_bo_allocations=part_filters.annotate_variant_quantity(
variant_stock_query, reference='sales_order_allocations__quantity'
),
- variant_so_allocations=part.filters.annotate_variant_quantity(
+ variant_so_allocations=part_filters.annotate_variant_quantity(
variant_stock_query, reference='allocations__quantity'
),
)
diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx
index 1c6a9c9e3f..062fbf0789 100644
--- a/src/frontend/src/pages/build/BuildDetail.tsx
+++ b/src/frontend/src/pages/build/BuildDetail.tsx
@@ -247,11 +247,7 @@ export default function BuildDetail() {
label: t`Line Items`,
icon: ,
content: build?.pk ? (
-
+
) : (
)
diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx
index 70448e26d4..64cb40df98 100644
--- a/src/frontend/src/pages/part/PartDetail.tsx
+++ b/src/frontend/src/pages/part/PartDetail.tsx
@@ -543,7 +543,7 @@ export default function PartDetail() {
label: t`Variants`,
icon: ,
hidden: !part.is_template,
- content:
+ content:
},
{
name: 'allocations',
diff --git a/src/frontend/src/tables/build/BuildLineTable.tsx b/src/frontend/src/tables/build/BuildLineTable.tsx
index 5d0b6c90ac..1940a95de2 100644
--- a/src/frontend/src/tables/build/BuildLineTable.tsx
+++ b/src/frontend/src/tables/build/BuildLineTable.tsx
@@ -5,11 +5,14 @@ import {
IconShoppingCart,
IconTool
} from '@tabler/icons-react';
-import { useCallback, useMemo } from 'react';
+import { useCallback, useMemo, useState } from 'react';
import { ProgressBar } from '../../components/items/ProgressBar';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
+import { UserRoles } from '../../enums/Roles';
+import { useBuildOrderFields } from '../../forms/BuildForms';
+import { useCreateApiFormModal } from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
@@ -19,7 +22,13 @@ import { TableFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
import { TableHoverCard } from '../TableHoverCard';
-export default function BuildLineTable({ params = {} }: { params?: any }) {
+export default function BuildLineTable({
+ buildId,
+ params = {}
+}: {
+ buildId: number;
+ params?: any;
+}) {
const table = useTable('buildline');
const user = useUserState();
@@ -213,6 +222,19 @@ export default function BuildLineTable({ params = {} }: { params?: any }) {
];
}, []);
+ const buildOrderFields = useBuildOrderFields({ create: true });
+
+ const [initialData, setInitialData] = useState({});
+
+ const newBuildOrder = useCreateApiFormModal({
+ url: ApiEndpoints.build_order_list,
+ title: t`Create Build Order`,
+ fields: buildOrderFields,
+ initialData: initialData,
+ follow: true,
+ modelType: ModelType.build
+ });
+
const rowActions = useCallback(
(record: any) => {
let part = record.part_detail;
@@ -243,8 +265,16 @@ export default function BuildLineTable({ params = {} }: { params?: any }) {
{
icon: ,
title: t`Build Stock`,
- hidden: !part?.assembly,
- color: 'blue'
+ hidden: !part?.assembly || !user.hasAddRole(UserRoles.build),
+ color: 'blue',
+ onClick: () => {
+ setInitialData({
+ part: record.part,
+ parent: buildId,
+ quantity: record.quantity - record.allocated
+ });
+ newBuildOrder.open();
+ }
}
];
},
@@ -252,21 +282,25 @@ export default function BuildLineTable({ params = {} }: { params?: any }) {
);
return (
-
+ <>
+ {newBuildOrder.modal}
+
+ >
);
}
diff --git a/src/frontend/src/tables/part/PartTable.tsx b/src/frontend/src/tables/part/PartTable.tsx
index b18cd436ec..49bbe87c6d 100644
--- a/src/frontend/src/tables/part/PartTable.tsx
+++ b/src/frontend/src/tables/part/PartTable.tsx
@@ -303,20 +303,28 @@ function partTableFilters(): TableFilter[] {
* @param {Object} params - The query parameters to pass to the API
* @returns
*/
-export function PartListTable({ props }: { props: InvenTreeTableProps }) {
+export function PartListTable({
+ props,
+ defaultPartData
+}: {
+ props: InvenTreeTableProps;
+ defaultPartData?: any;
+}) {
const tableColumns = useMemo(() => partTableColumns(), []);
const tableFilters = useMemo(() => partTableFilters(), []);
const table = useTable('part-list');
const user = useUserState();
+ const initialPartData = useMemo(() => {
+ return defaultPartData ?? props.params ?? {};
+ }, [defaultPartData, props.params]);
+
const newPart = useCreateApiFormModal({
url: ApiEndpoints.part_list,
title: t`Add Part`,
fields: usePartFields({ create: true }),
- initialData: {
- ...(props.params ?? {})
- },
+ initialData: initialPartData,
follow: true,
modelType: ModelType.part
});
diff --git a/src/frontend/src/tables/part/PartVariantTable.tsx b/src/frontend/src/tables/part/PartVariantTable.tsx
index a8c8df2251..72487005c4 100644
--- a/src/frontend/src/tables/part/PartVariantTable.tsx
+++ b/src/frontend/src/tables/part/PartVariantTable.tsx
@@ -7,7 +7,7 @@ import { PartListTable } from './PartTable';
/**
* Display variant parts for the specified parent part
*/
-export function PartVariantTable({ partId }: { partId: string }) {
+export function PartVariantTable({ part }: { part: any }) {
const tableFilters: TableFilter[] = useMemo(() => {
return [
{
@@ -39,9 +39,14 @@ export function PartVariantTable({ partId }: { partId: string }) {
enableDownload: false,
tableFilters: tableFilters,
params: {
- ancestor: partId
+ ancestor: part.pk
}
}}
+ defaultPartData={{
+ ...part,
+ variant_of: part.pk,
+ is_template: false
+ }}
/>
);
}
diff --git a/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx b/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx
index 5beff3b52b..d4dad568e0 100644
--- a/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx
+++ b/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx
@@ -13,6 +13,7 @@ import { formatCurrency } from '../../defaults/formatters';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
+import { useBuildOrderFields } from '../../forms/BuildForms';
import { useSalesOrderLineItemFields } from '../../forms/SalesOrderForms';
import {
useCreateApiFormModal,
@@ -122,6 +123,22 @@ export default function SalesOrderLineItemTable({
extra.push({t`Includes variant stock`});
}
+ if (record.building > 0) {
+ extra.push(
+
+ {t`In production`}: {record.building}
+
+ );
+ }
+
+ if (record.on_order > 0) {
+ extra.push(
+
+ {t`On order`}: {record.on_order}
+
+ );
+ }
+
return (
{text}}
@@ -199,6 +216,17 @@ export default function SalesOrderLineItemTable({
table: table
});
+ const buildOrderFields = useBuildOrderFields({ create: true });
+
+ const newBuildOrder = useCreateApiFormModal({
+ url: ApiEndpoints.build_order_list,
+ title: t`Create Build Order`,
+ fields: buildOrderFields,
+ initialData: initialData,
+ follow: true,
+ modelType: ModelType.build
+ });
+
const tableActions = useMemo(() => {
return [
,
- color: 'blue'
+ color: 'blue',
+ onClick: () => {
+ setInitialData({
+ part: record.part,
+ quantity: (record?.quantity ?? 1) - (record?.allocated ?? 0),
+ sales_order: orderId
+ });
+ newBuildOrder.open();
+ }
},
{
hidden:
@@ -277,6 +313,7 @@ export default function SalesOrderLineItemTable({
{editLine.modal}
{deleteLine.modal}
{newLine.modal}
+ {newBuildOrder.modal}
{
await page.getByRole('tab', { name: 'Build', exact: true }).click();
// We have now loaded the "Build Order" table. Check for some expected texts
- await page.getByText('On Hold').waitFor();
+ await page.getByText('On Hold').first().waitFor();
await page.getByText('Pending').first().waitFor();
// Load a particular build order