mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 13:15:43 +00:00 
			
		
		
		
	[PUI/Feature] Integrate Part "Default Location" into UX (#5972)
* Add default parts to location page * Fix name strings * Add Stock Transfer modal * Add ApiForm Table field * temp * Add stock transfer form to part, stock item and location * All stock operations for Item, Part, and Location added (except order new) * Add default_location category traversal, and initial PO Line Item Receive form * . * Remove debug values * Added PO line receive form * Add functionality to PO receive extra fields * . * Forgot to bump API version * Add Category Default to details panel * Fix stockItem query count * Fix reviewed issues * . * . * . * Prevent root category from checking parent for default location
This commit is contained in:
		| @@ -1,12 +1,18 @@ | ||||
| """InvenTree API version information.""" | ||||
|  | ||||
| # InvenTree API version | ||||
| INVENTREE_API_VERSION = 182 | ||||
| INVENTREE_API_VERSION = 183 | ||||
| """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" | ||||
|  | ||||
| INVENTREE_API_TEXT = """ | ||||
|  | ||||
| v182 - 2024-03-15 : https://github.com/inventree/InvenTree/pull/6714 | ||||
| v183 - 2024-03-14 : https://github.com/inventree/InvenTree/pull/5972 | ||||
|     - Adds "category_default_location" annotated field to part serializer | ||||
|     - Adds "part_detail.category_default_location" annotated field to stock item serializer | ||||
|     - Adds "part_detail.category_default_location" annotated field to purchase order line serializer | ||||
|     - Adds "parent_default_location" annotated field to category serializer | ||||
|  | ||||
| v182 - 2024-03-13 : https://github.com/inventree/InvenTree/pull/6714 | ||||
|     - Expose ReportSnippet model to the /report/snippet/ API endpoint | ||||
|     - Expose ReportAsset model to the /report/asset/ API endpoint | ||||
|  | ||||
|   | ||||
| @@ -5,7 +5,16 @@ from decimal import Decimal | ||||
|  | ||||
| from django.core.exceptions import ValidationError as DjangoValidationError | ||||
| from django.db import models, transaction | ||||
| from django.db.models import BooleanField, Case, ExpressionWrapper, F, Q, Value, When | ||||
| from django.db.models import ( | ||||
|     BooleanField, | ||||
|     Case, | ||||
|     ExpressionWrapper, | ||||
|     F, | ||||
|     Prefetch, | ||||
|     Q, | ||||
|     Value, | ||||
|     When, | ||||
| ) | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
|  | ||||
| from rest_framework import serializers | ||||
| @@ -14,6 +23,8 @@ 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 | ||||
| import stock.serializers | ||||
| from common.serializers import ProjectCodeSerializer | ||||
| @@ -375,6 +386,17 @@ class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer): | ||||
|         - "total_price" = purchase_price * quantity | ||||
|         - "overdue" status (boolean field) | ||||
|         """ | ||||
|         queryset = queryset.prefetch_related( | ||||
|             Prefetch( | ||||
|                 'part__part', | ||||
|                 queryset=part_models.Part.objects.annotate( | ||||
|                     category_default_location=part_filters.annotate_default_location( | ||||
|                         'category__' | ||||
|                     ) | ||||
|                 ).prefetch_related(None), | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|         queryset = queryset.annotate( | ||||
|             total_price=ExpressionWrapper( | ||||
|                 F('purchase_price') * F('quantity'), output_field=models.DecimalField() | ||||
|   | ||||
| @@ -287,6 +287,32 @@ def annotate_category_parts(): | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def annotate_default_location(reference=''): | ||||
|     """Construct a queryset that finds the closest default location in the part's category tree. | ||||
|  | ||||
|     If the part's category has its own default_location, this is returned. | ||||
|     If not, the category tree is traversed until a value is found. | ||||
|     """ | ||||
|     subquery = part.models.PartCategory.objects.filter( | ||||
|         tree_id=OuterRef(f'{reference}tree_id'), | ||||
|         lft__lt=OuterRef(f'{reference}lft'), | ||||
|         rght__gt=OuterRef(f'{reference}rght'), | ||||
|         level__lte=OuterRef(f'{reference}level'), | ||||
|         parent__isnull=False, | ||||
|     ) | ||||
|  | ||||
|     return Coalesce( | ||||
|         F(f'{reference}default_location'), | ||||
|         Subquery( | ||||
|             subquery.order_by('-level') | ||||
|             .filter(default_location__isnull=False) | ||||
|             .values('default_location') | ||||
|         ), | ||||
|         Value(None), | ||||
|         output_field=IntegerField(), | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def annotate_sub_categories(): | ||||
|     """Construct a queryset annotation which returns the number of subcategories for each provided category.""" | ||||
|     subquery = part.models.PartCategory.objects.filter( | ||||
|   | ||||
| @@ -81,6 +81,7 @@ class CategorySerializer(InvenTree.serializers.InvenTreeModelSerializer): | ||||
|             'url', | ||||
|             'structural', | ||||
|             'icon', | ||||
|             'parent_default_location', | ||||
|         ] | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
| @@ -105,6 +106,10 @@ class CategorySerializer(InvenTree.serializers.InvenTreeModelSerializer): | ||||
|             subcategories=part.filters.annotate_sub_categories(), | ||||
|         ) | ||||
|  | ||||
|         queryset = queryset.annotate( | ||||
|             parent_default_location=part.filters.annotate_default_location('parent__') | ||||
|         ) | ||||
|  | ||||
|         return queryset | ||||
|  | ||||
|     url = serializers.CharField(source='get_absolute_url', read_only=True) | ||||
| @@ -121,6 +126,8 @@ class CategorySerializer(InvenTree.serializers.InvenTreeModelSerializer): | ||||
|         child=serializers.DictField(), source='get_path', read_only=True | ||||
|     ) | ||||
|  | ||||
|     parent_default_location = serializers.IntegerField(read_only=True) | ||||
|  | ||||
|  | ||||
| class CategoryTree(InvenTree.serializers.InvenTreeModelSerializer): | ||||
|     """Serializer for PartCategory tree.""" | ||||
| @@ -283,6 +290,7 @@ class PartBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer): | ||||
|             'pk', | ||||
|             'IPN', | ||||
|             'barcode_hash', | ||||
|             'category_default_location', | ||||
|             'default_location', | ||||
|             'name', | ||||
|             'revision', | ||||
| @@ -314,6 +322,8 @@ class PartBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer): | ||||
|             self.fields.pop('pricing_min') | ||||
|             self.fields.pop('pricing_max') | ||||
|  | ||||
|     category_default_location = serializers.IntegerField(read_only=True) | ||||
|  | ||||
|     image = InvenTree.serializers.InvenTreeImageSerializerField(read_only=True) | ||||
|     thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True) | ||||
|  | ||||
| @@ -611,6 +621,7 @@ class PartSerializer( | ||||
|             'allocated_to_build_orders', | ||||
|             'allocated_to_sales_orders', | ||||
|             'building', | ||||
|             'category_default_location', | ||||
|             'in_stock', | ||||
|             'ordering', | ||||
|             'required_for_build_orders', | ||||
| @@ -766,6 +777,12 @@ class PartSerializer( | ||||
|             required_for_sales_orders=part.filters.annotate_sales_order_requirements(), | ||||
|         ) | ||||
|  | ||||
|         queryset = queryset.annotate( | ||||
|             category_default_location=part.filters.annotate_default_location( | ||||
|                 'category__' | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|         return queryset | ||||
|  | ||||
|     def get_starred(self, part) -> bool: | ||||
| @@ -805,6 +822,7 @@ class PartSerializer( | ||||
|     unallocated_stock = serializers.FloatField( | ||||
|         read_only=True, label=_('Unallocated Stock') | ||||
|     ) | ||||
|     category_default_location = serializers.IntegerField(read_only=True) | ||||
|     variant_stock = serializers.FloatField(read_only=True, label=_('Variant Stock')) | ||||
|  | ||||
|     minimum_stock = serializers.FloatField() | ||||
|   | ||||
| @@ -6,7 +6,7 @@ from decimal import Decimal | ||||
|  | ||||
| from django.core.exceptions import ValidationError as DjangoValidationError | ||||
| from django.db import transaction | ||||
| from django.db.models import BooleanField, Case, Count, Q, Value, When | ||||
| from django.db.models import BooleanField, Case, Count, Prefetch, Q, Value, When | ||||
| from django.db.models.functions import Coalesce | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
|  | ||||
| @@ -20,6 +20,7 @@ import company.models | ||||
| import InvenTree.helpers | ||||
| import InvenTree.serializers | ||||
| import InvenTree.status_codes | ||||
| import part.filters as part_filters | ||||
| import part.models as part_models | ||||
| import stock.filters | ||||
| from company.serializers import SupplierPartSerializer | ||||
| @@ -289,7 +290,14 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer): | ||||
|             'location', | ||||
|             'sales_order', | ||||
|             'purchase_order', | ||||
|             'part', | ||||
|             Prefetch( | ||||
|                 'part', | ||||
|                 queryset=part_models.Part.objects.annotate( | ||||
|                     category_default_location=part_filters.annotate_default_location( | ||||
|                         'category__' | ||||
|                     ) | ||||
|                 ).prefetch_related(None), | ||||
|             ), | ||||
|             'part__category', | ||||
|             'part__pricing_data', | ||||
|             'supplier_part', | ||||
|   | ||||
		Reference in New Issue
	
	Block a user