mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-03 22:55:43 +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:
		@@ -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,21 +282,25 @@ export default function BuildLineTable({ params = {} }: { params?: any }) {
 | 
				
			|||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <InvenTreeTable
 | 
					    <>
 | 
				
			||||||
      url={apiUrl(ApiEndpoints.build_line_list)}
 | 
					      {newBuildOrder.modal}
 | 
				
			||||||
      tableState={table}
 | 
					      <InvenTreeTable
 | 
				
			||||||
      columns={tableColumns}
 | 
					        url={apiUrl(ApiEndpoints.build_line_list)}
 | 
				
			||||||
      props={{
 | 
					        tableState={table}
 | 
				
			||||||
        params: {
 | 
					        columns={tableColumns}
 | 
				
			||||||
          ...params,
 | 
					        props={{
 | 
				
			||||||
          part_detail: true
 | 
					          params: {
 | 
				
			||||||
        },
 | 
					            ...params,
 | 
				
			||||||
        tableFilters: tableFilters,
 | 
					            build: buildId,
 | 
				
			||||||
        rowActions: rowActions,
 | 
					            part_detail: true
 | 
				
			||||||
        modelType: ModelType.part,
 | 
					          },
 | 
				
			||||||
        modelField: 'part_detail.pk',
 | 
					          tableFilters: tableFilters,
 | 
				
			||||||
        enableDownload: true
 | 
					          rowActions: rowActions,
 | 
				
			||||||
      }}
 | 
					          modelType: ModelType.part,
 | 
				
			||||||
    />
 | 
					          modelField: 'part_detail.pk',
 | 
				
			||||||
 | 
					          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
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user