diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8b340ebe93..bfc2314a9f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: - id: check-yaml - id: mixed-line-ending - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.2.2 + rev: v0.3.0 hooks: - id: ruff-format args: [--preview] @@ -26,7 +26,7 @@ repos: --preview ] - repo: https://github.com/matmair/ruff-pre-commit - rev: 830893bf46db844d9c99b6c468e285199adf2de6 # uv-018 + rev: 8bed1087452bdf816b840ea7b6848b21d32b7419 # uv-018 hooks: - id: pip-compile name: pip-compile requirements-dev.in @@ -60,7 +60,7 @@ repos: - "prettier@^2.4.1" - "@trivago/prettier-plugin-sort-imports" - repo: https://github.com/pre-commit/mirrors-eslint - rev: "v9.0.0-beta.0" + rev: "v9.0.0-beta.1" hooks: - id: eslint additional_dependencies: diff --git a/InvenTree/InvenTree/api_version.py b/InvenTree/InvenTree/api_version.py index 7d62e9b909..1f12b4d7bc 100644 --- a/InvenTree/InvenTree/api_version.py +++ b/InvenTree/InvenTree/api_version.py @@ -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 diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 3405e7b8b8..90f9aa0a0d 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -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() diff --git a/InvenTree/part/filters.py b/InvenTree/part/filters.py index 42a41eb923..4d247529b9 100644 --- a/InvenTree/part/filters.py +++ b/InvenTree/part/filters.py @@ -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( diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 38e2b7157d..f02ade9ed9 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -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() diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 88ed8e05a2..8d062f6855 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -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', diff --git a/src/frontend/src/components/forms/ApiForm.tsx b/src/frontend/src/components/forms/ApiForm.tsx index 2b07aabf9f..53fdabe9c7 100644 --- a/src/frontend/src/components/forms/ApiForm.tsx +++ b/src/frontend/src/components/forms/ApiForm.tsx @@ -443,7 +443,7 @@ export function ApiForm({ id, props }: { id: string; props: ApiFormProps }) { ))}