mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
[PUI] Sales order actions (#7837)
* Create build order from sales order table * Allow creation of child build order from build page * Add production and purcahse order quantitres to sales order item serializer * Bump API version * Fix playwright test
This commit is contained in:
parent
a5564090bb
commit
21f623eea8
@ -1,13 +1,17 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# 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."""
|
"""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 = """
|
||||||
|
|
||||||
|
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
|
v234 - 2024-08-08 : https://github.com/inventree/InvenTree/pull/7829
|
||||||
- Fixes bug in the plugin metadata endpoint
|
- Fixes bug in the plugin metadata endpoint
|
||||||
|
|
||||||
|
@ -21,7 +21,6 @@ from rest_framework.serializers import ValidationError
|
|||||||
from sql_util.utils import SubqueryCount
|
from sql_util.utils import SubqueryCount
|
||||||
|
|
||||||
import order.models
|
import order.models
|
||||||
import part.filters
|
|
||||||
import part.filters as part_filters
|
import part.filters as part_filters
|
||||||
import part.models as part_models
|
import part.models as part_models
|
||||||
import stock.models
|
import stock.models
|
||||||
@ -1030,8 +1029,6 @@ class SalesOrderLineItemSerializer(
|
|||||||
'pk',
|
'pk',
|
||||||
'allocated',
|
'allocated',
|
||||||
'allocations',
|
'allocations',
|
||||||
'available_stock',
|
|
||||||
'available_variant_stock',
|
|
||||||
'customer_detail',
|
'customer_detail',
|
||||||
'quantity',
|
'quantity',
|
||||||
'reference',
|
'reference',
|
||||||
@ -1046,6 +1043,11 @@ class SalesOrderLineItemSerializer(
|
|||||||
'shipped',
|
'shipped',
|
||||||
'target_date',
|
'target_date',
|
||||||
'link',
|
'link',
|
||||||
|
# Annotated fields for part stocking information
|
||||||
|
'available_stock',
|
||||||
|
'available_variant_stock',
|
||||||
|
'building',
|
||||||
|
'on_order',
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -1078,6 +1080,8 @@ class SalesOrderLineItemSerializer(
|
|||||||
|
|
||||||
- "overdue" status (boolean field)
|
- "overdue" status (boolean field)
|
||||||
- "available_quantity"
|
- "available_quantity"
|
||||||
|
- "building"
|
||||||
|
- "on_order"
|
||||||
"""
|
"""
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
overdue=Case(
|
overdue=Case(
|
||||||
@ -1093,11 +1097,11 @@ class SalesOrderLineItemSerializer(
|
|||||||
# Annotate each line with the available stock quantity
|
# Annotate each line with the available stock quantity
|
||||||
# To do this, we need to look at the total stock and any allocations
|
# To do this, we need to look at the total stock and any allocations
|
||||||
queryset = queryset.alias(
|
queryset = queryset.alias(
|
||||||
total_stock=part.filters.annotate_total_stock(reference='part__'),
|
total_stock=part_filters.annotate_total_stock(reference='part__'),
|
||||||
allocated_to_sales_orders=part.filters.annotate_sales_order_allocations(
|
allocated_to_sales_orders=part_filters.annotate_sales_order_allocations(
|
||||||
reference='part__'
|
reference='part__'
|
||||||
),
|
),
|
||||||
allocated_to_build_orders=part.filters.annotate_build_order_allocations(
|
allocated_to_build_orders=part_filters.annotate_build_order_allocations(
|
||||||
reference='part__'
|
reference='part__'
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -1112,19 +1116,19 @@ class SalesOrderLineItemSerializer(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Filter for "variant" stock: Variant stock items must be salable and active
|
# 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__'
|
reference='part__'
|
||||||
).filter(part__salable=True, part__active=True)
|
).filter(part__salable=True, part__active=True)
|
||||||
|
|
||||||
# Also add in available "variant" stock
|
# Also add in available "variant" stock
|
||||||
queryset = queryset.alias(
|
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_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_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'
|
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
|
return queryset
|
||||||
|
|
||||||
customer_detail = CompanyBriefSerializer(
|
customer_detail = CompanyBriefSerializer(
|
||||||
@ -1153,6 +1167,8 @@ class SalesOrderLineItemSerializer(
|
|||||||
overdue = serializers.BooleanField(required=False, read_only=True)
|
overdue = serializers.BooleanField(required=False, read_only=True)
|
||||||
available_stock = serializers.FloatField(read_only=True)
|
available_stock = serializers.FloatField(read_only=True)
|
||||||
available_variant_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()
|
quantity = InvenTreeDecimalField()
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ import company.models
|
|||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
import InvenTree.serializers
|
import InvenTree.serializers
|
||||||
import InvenTree.status
|
import InvenTree.status
|
||||||
import part.filters
|
import part.filters as part_filters
|
||||||
import part.helpers as part_helpers
|
import part.helpers as part_helpers
|
||||||
import part.stocktake
|
import part.stocktake
|
||||||
import part.tasks
|
import part.tasks
|
||||||
@ -107,12 +107,12 @@ class CategorySerializer(
|
|||||||
"""Annotate extra information to the queryset."""
|
"""Annotate extra information to the queryset."""
|
||||||
# Annotate the number of 'parts' which exist in each category (including subcategories!)
|
# Annotate the number of 'parts' which exist in each category (including subcategories!)
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
part_count=part.filters.annotate_category_parts(),
|
part_count=part_filters.annotate_category_parts(),
|
||||||
subcategories=part.filters.annotate_sub_categories(),
|
subcategories=part_filters.annotate_sub_categories(),
|
||||||
)
|
)
|
||||||
|
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
parent_default_location=part.filters.annotate_default_location('parent__')
|
parent_default_location=part_filters.annotate_default_location('parent__')
|
||||||
)
|
)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
@ -164,7 +164,7 @@ class CategoryTree(InvenTree.serializers.InvenTreeModelSerializer):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def annotate_queryset(queryset):
|
def annotate_queryset(queryset):
|
||||||
"""Annotate the queryset with the number of subcategories."""
|
"""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()
|
@register_importer()
|
||||||
@ -781,10 +781,10 @@ class PartSerializer(
|
|||||||
queryset = queryset.annotate(stock_item_count=SubqueryCount('stock_items'))
|
queryset = queryset.annotate(stock_item_count=SubqueryCount('stock_items'))
|
||||||
|
|
||||||
# Annotate with the total variant stock quantity
|
# Annotate with the total variant stock quantity
|
||||||
variant_query = part.filters.variant_stock_query()
|
variant_query = part_filters.variant_stock_query()
|
||||||
|
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
variant_stock=part.filters.annotate_variant_quantity(
|
variant_stock=part_filters.annotate_variant_quantity(
|
||||||
variant_query, reference='quantity'
|
variant_query, reference='quantity'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -814,10 +814,10 @@ class PartSerializer(
|
|||||||
# TODO: Note that BomItemSerializer and BuildLineSerializer have very similar code
|
# TODO: Note that BomItemSerializer and BuildLineSerializer have very similar code
|
||||||
|
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
ordering=part.filters.annotate_on_order_quantity(),
|
ordering=part_filters.annotate_on_order_quantity(),
|
||||||
in_stock=part.filters.annotate_total_stock(),
|
in_stock=part_filters.annotate_total_stock(),
|
||||||
allocated_to_sales_orders=part.filters.annotate_sales_order_allocations(),
|
allocated_to_sales_orders=part_filters.annotate_sales_order_allocations(),
|
||||||
allocated_to_build_orders=part.filters.annotate_build_order_allocations(),
|
allocated_to_build_orders=part_filters.annotate_build_order_allocations(),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Annotate the queryset with the 'total_in_stock' quantity
|
# Annotate the queryset with the 'total_in_stock' quantity
|
||||||
@ -829,7 +829,7 @@ class PartSerializer(
|
|||||||
)
|
)
|
||||||
|
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
external_stock=part.filters.annotate_total_stock(
|
external_stock=part_filters.annotate_total_stock(
|
||||||
filter=Q(location__external=True)
|
filter=Q(location__external=True)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -847,12 +847,12 @@ class PartSerializer(
|
|||||||
|
|
||||||
# Annotate with the total 'required for builds' quantity
|
# Annotate with the total 'required for builds' quantity
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
required_for_build_orders=part.filters.annotate_build_order_requirements(),
|
required_for_build_orders=part_filters.annotate_build_order_requirements(),
|
||||||
required_for_sales_orders=part.filters.annotate_sales_order_requirements(),
|
required_for_sales_orders=part_filters.annotate_sales_order_requirements(),
|
||||||
)
|
)
|
||||||
|
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
category_default_location=part.filters.annotate_default_location(
|
category_default_location=part_filters.annotate_default_location(
|
||||||
'category__'
|
'category__'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -1684,30 +1684,23 @@ class BomItemSerializer(
|
|||||||
|
|
||||||
# Annotate with the total "on order" amount for the sub-part
|
# Annotate with the total "on order" amount for the sub-part
|
||||||
queryset = queryset.annotate(
|
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
|
# Annotate with the total "building" amount for the sub-part
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
building=Coalesce(
|
building=part_filters.annotate_in_production_quantity(ref)
|
||||||
SubquerySum(
|
|
||||||
'sub_part__builds__quantity',
|
|
||||||
filter=Q(status__in=BuildStatusGroups.ACTIVE_CODES),
|
|
||||||
),
|
|
||||||
Decimal(0),
|
|
||||||
output_field=models.DecimalField(),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Calculate "total stock" for the referenced sub_part
|
# Calculate "total stock" for the referenced sub_part
|
||||||
# Calculate the "build_order_allocations" for the sub_part
|
# Calculate the "build_order_allocations" for the sub_part
|
||||||
# Note that these fields are only aliased, not annotated
|
# Note that these fields are only aliased, not annotated
|
||||||
queryset = queryset.alias(
|
queryset = queryset.alias(
|
||||||
total_stock=part.filters.annotate_total_stock(reference=ref),
|
total_stock=part_filters.annotate_total_stock(reference=ref),
|
||||||
allocated_to_sales_orders=part.filters.annotate_sales_order_allocations(
|
allocated_to_sales_orders=part_filters.annotate_sales_order_allocations(
|
||||||
reference=ref
|
reference=ref
|
||||||
),
|
),
|
||||||
allocated_to_build_orders=part.filters.annotate_build_order_allocations(
|
allocated_to_build_orders=part_filters.annotate_build_order_allocations(
|
||||||
reference=ref
|
reference=ref
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -1724,7 +1717,7 @@ class BomItemSerializer(
|
|||||||
|
|
||||||
# Calculate 'external_stock'
|
# Calculate 'external_stock'
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
external_stock=part.filters.annotate_total_stock(
|
external_stock=part_filters.annotate_total_stock(
|
||||||
reference=ref, filter=Q(location__external=True)
|
reference=ref, filter=Q(location__external=True)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -1733,11 +1726,11 @@ class BomItemSerializer(
|
|||||||
|
|
||||||
# Extract similar information for any 'substitute' parts
|
# Extract similar information for any 'substitute' parts
|
||||||
queryset = queryset.alias(
|
queryset = queryset.alias(
|
||||||
substitute_stock=part.filters.annotate_total_stock(reference=ref),
|
substitute_stock=part_filters.annotate_total_stock(reference=ref),
|
||||||
substitute_build_allocations=part.filters.annotate_build_order_allocations(
|
substitute_build_allocations=part_filters.annotate_build_order_allocations(
|
||||||
reference=ref
|
reference=ref
|
||||||
),
|
),
|
||||||
substitute_sales_allocations=part.filters.annotate_sales_order_allocations(
|
substitute_sales_allocations=part_filters.annotate_sales_order_allocations(
|
||||||
reference=ref
|
reference=ref
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -1753,16 +1746,16 @@ class BomItemSerializer(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Annotate the queryset with 'available variant stock' information
|
# 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(
|
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_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_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'
|
variant_stock_query, reference='allocations__quantity'
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -247,11 +247,7 @@ export default function BuildDetail() {
|
|||||||
label: t`Line Items`,
|
label: t`Line Items`,
|
||||||
icon: <IconListNumbers />,
|
icon: <IconListNumbers />,
|
||||||
content: build?.pk ? (
|
content: build?.pk ? (
|
||||||
<BuildLineTable
|
<BuildLineTable buildId={build.pk} />
|
||||||
params={{
|
|
||||||
build: id
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<Skeleton />
|
<Skeleton />
|
||||||
)
|
)
|
||||||
|
@ -543,7 +543,7 @@ export default function PartDetail() {
|
|||||||
label: t`Variants`,
|
label: t`Variants`,
|
||||||
icon: <IconVersions />,
|
icon: <IconVersions />,
|
||||||
hidden: !part.is_template,
|
hidden: !part.is_template,
|
||||||
content: <PartVariantTable partId={String(id)} />
|
content: <PartVariantTable part={part} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'allocations',
|
name: 'allocations',
|
||||||
|
@ -5,11 +5,14 @@ import {
|
|||||||
IconShoppingCart,
|
IconShoppingCart,
|
||||||
IconTool
|
IconTool
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { ProgressBar } from '../../components/items/ProgressBar';
|
import { ProgressBar } from '../../components/items/ProgressBar';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
import { ModelType } from '../../enums/ModelType';
|
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 { useTable } from '../../hooks/UseTable';
|
||||||
import { apiUrl } from '../../states/ApiState';
|
import { apiUrl } from '../../states/ApiState';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
@ -19,7 +22,13 @@ import { TableFilter } from '../Filter';
|
|||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
import { TableHoverCard } from '../TableHoverCard';
|
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 table = useTable('buildline');
|
||||||
const user = useUserState();
|
const user = useUserState();
|
||||||
|
|
||||||
@ -213,6 +222,19 @@ export default function BuildLineTable({ params = {} }: { params?: any }) {
|
|||||||
];
|
];
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const buildOrderFields = useBuildOrderFields({ create: true });
|
||||||
|
|
||||||
|
const [initialData, setInitialData] = useState<any>({});
|
||||||
|
|
||||||
|
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(
|
const rowActions = useCallback(
|
||||||
(record: any) => {
|
(record: any) => {
|
||||||
let part = record.part_detail;
|
let part = record.part_detail;
|
||||||
@ -243,8 +265,16 @@ export default function BuildLineTable({ params = {} }: { params?: any }) {
|
|||||||
{
|
{
|
||||||
icon: <IconTool />,
|
icon: <IconTool />,
|
||||||
title: t`Build Stock`,
|
title: t`Build Stock`,
|
||||||
hidden: !part?.assembly,
|
hidden: !part?.assembly || !user.hasAddRole(UserRoles.build),
|
||||||
color: 'blue'
|
color: 'blue',
|
||||||
|
onClick: () => {
|
||||||
|
setInitialData({
|
||||||
|
part: record.part,
|
||||||
|
parent: buildId,
|
||||||
|
quantity: record.quantity - record.allocated
|
||||||
|
});
|
||||||
|
newBuildOrder.open();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
@ -252,6 +282,8 @@ export default function BuildLineTable({ params = {} }: { params?: any }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{newBuildOrder.modal}
|
||||||
<InvenTreeTable
|
<InvenTreeTable
|
||||||
url={apiUrl(ApiEndpoints.build_line_list)}
|
url={apiUrl(ApiEndpoints.build_line_list)}
|
||||||
tableState={table}
|
tableState={table}
|
||||||
@ -259,6 +291,7 @@ export default function BuildLineTable({ params = {} }: { params?: any }) {
|
|||||||
props={{
|
props={{
|
||||||
params: {
|
params: {
|
||||||
...params,
|
...params,
|
||||||
|
build: buildId,
|
||||||
part_detail: true
|
part_detail: true
|
||||||
},
|
},
|
||||||
tableFilters: tableFilters,
|
tableFilters: tableFilters,
|
||||||
@ -268,5 +301,6 @@ export default function BuildLineTable({ params = {} }: { params?: any }) {
|
|||||||
enableDownload: true
|
enableDownload: true
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -303,20 +303,28 @@ function partTableFilters(): TableFilter[] {
|
|||||||
* @param {Object} params - The query parameters to pass to the API
|
* @param {Object} params - The query parameters to pass to the API
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function PartListTable({ props }: { props: InvenTreeTableProps }) {
|
export function PartListTable({
|
||||||
|
props,
|
||||||
|
defaultPartData
|
||||||
|
}: {
|
||||||
|
props: InvenTreeTableProps;
|
||||||
|
defaultPartData?: any;
|
||||||
|
}) {
|
||||||
const tableColumns = useMemo(() => partTableColumns(), []);
|
const tableColumns = useMemo(() => partTableColumns(), []);
|
||||||
const tableFilters = useMemo(() => partTableFilters(), []);
|
const tableFilters = useMemo(() => partTableFilters(), []);
|
||||||
|
|
||||||
const table = useTable('part-list');
|
const table = useTable('part-list');
|
||||||
const user = useUserState();
|
const user = useUserState();
|
||||||
|
|
||||||
|
const initialPartData = useMemo(() => {
|
||||||
|
return defaultPartData ?? props.params ?? {};
|
||||||
|
}, [defaultPartData, props.params]);
|
||||||
|
|
||||||
const newPart = useCreateApiFormModal({
|
const newPart = useCreateApiFormModal({
|
||||||
url: ApiEndpoints.part_list,
|
url: ApiEndpoints.part_list,
|
||||||
title: t`Add Part`,
|
title: t`Add Part`,
|
||||||
fields: usePartFields({ create: true }),
|
fields: usePartFields({ create: true }),
|
||||||
initialData: {
|
initialData: initialPartData,
|
||||||
...(props.params ?? {})
|
|
||||||
},
|
|
||||||
follow: true,
|
follow: true,
|
||||||
modelType: ModelType.part
|
modelType: ModelType.part
|
||||||
});
|
});
|
||||||
|
@ -7,7 +7,7 @@ import { PartListTable } from './PartTable';
|
|||||||
/**
|
/**
|
||||||
* Display variant parts for the specified parent part
|
* Display variant parts for the specified parent part
|
||||||
*/
|
*/
|
||||||
export function PartVariantTable({ partId }: { partId: string }) {
|
export function PartVariantTable({ part }: { part: any }) {
|
||||||
const tableFilters: TableFilter[] = useMemo(() => {
|
const tableFilters: TableFilter[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -39,9 +39,14 @@ export function PartVariantTable({ partId }: { partId: string }) {
|
|||||||
enableDownload: false,
|
enableDownload: false,
|
||||||
tableFilters: tableFilters,
|
tableFilters: tableFilters,
|
||||||
params: {
|
params: {
|
||||||
ancestor: partId
|
ancestor: part.pk
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
defaultPartData={{
|
||||||
|
...part,
|
||||||
|
variant_of: part.pk,
|
||||||
|
is_template: false
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ import { formatCurrency } from '../../defaults/formatters';
|
|||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
import { ModelType } from '../../enums/ModelType';
|
import { ModelType } from '../../enums/ModelType';
|
||||||
import { UserRoles } from '../../enums/Roles';
|
import { UserRoles } from '../../enums/Roles';
|
||||||
|
import { useBuildOrderFields } from '../../forms/BuildForms';
|
||||||
import { useSalesOrderLineItemFields } from '../../forms/SalesOrderForms';
|
import { useSalesOrderLineItemFields } from '../../forms/SalesOrderForms';
|
||||||
import {
|
import {
|
||||||
useCreateApiFormModal,
|
useCreateApiFormModal,
|
||||||
@ -122,6 +123,22 @@ export default function SalesOrderLineItemTable({
|
|||||||
extra.push(<Text size="sm">{t`Includes variant stock`}</Text>);
|
extra.push(<Text size="sm">{t`Includes variant stock`}</Text>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (record.building > 0) {
|
||||||
|
extra.push(
|
||||||
|
<Text size="sm">
|
||||||
|
{t`In production`}: {record.building}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (record.on_order > 0) {
|
||||||
|
extra.push(
|
||||||
|
<Text size="sm">
|
||||||
|
{t`On order`}: {record.on_order}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableHoverCard
|
<TableHoverCard
|
||||||
value={<Text color={color}>{text}</Text>}
|
value={<Text color={color}>{text}</Text>}
|
||||||
@ -199,6 +216,17 @@ export default function SalesOrderLineItemTable({
|
|||||||
table: table
|
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(() => {
|
const tableActions = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
<AddItemButton
|
<AddItemButton
|
||||||
@ -235,7 +263,15 @@ export default function SalesOrderLineItemTable({
|
|||||||
!record?.part_detail?.assembly,
|
!record?.part_detail?.assembly,
|
||||||
title: t`Build stock`,
|
title: t`Build stock`,
|
||||||
icon: <IconTools />,
|
icon: <IconTools />,
|
||||||
color: 'blue'
|
color: 'blue',
|
||||||
|
onClick: () => {
|
||||||
|
setInitialData({
|
||||||
|
part: record.part,
|
||||||
|
quantity: (record?.quantity ?? 1) - (record?.allocated ?? 0),
|
||||||
|
sales_order: orderId
|
||||||
|
});
|
||||||
|
newBuildOrder.open();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
hidden:
|
hidden:
|
||||||
@ -277,6 +313,7 @@ export default function SalesOrderLineItemTable({
|
|||||||
{editLine.modal}
|
{editLine.modal}
|
||||||
{deleteLine.modal}
|
{deleteLine.modal}
|
||||||
{newLine.modal}
|
{newLine.modal}
|
||||||
|
{newBuildOrder.modal}
|
||||||
<InvenTreeTable
|
<InvenTreeTable
|
||||||
url={apiUrl(ApiEndpoints.sales_order_line_list)}
|
url={apiUrl(ApiEndpoints.sales_order_line_list)}
|
||||||
tableState={table}
|
tableState={table}
|
||||||
|
@ -11,7 +11,7 @@ test('PUI - Pages - Build Order', async ({ page }) => {
|
|||||||
await page.getByRole('tab', { name: 'Build', exact: true }).click();
|
await page.getByRole('tab', { name: 'Build', exact: true }).click();
|
||||||
|
|
||||||
// We have now loaded the "Build Order" table. Check for some expected texts
|
// 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();
|
await page.getByText('Pending').first().waitFor();
|
||||||
|
|
||||||
// Load a particular build order
|
// Load a particular build order
|
||||||
|
Loading…
Reference in New Issue
Block a user